com.almende.eve.agent.google.GoogleTaskAgent.java Source code

Java tutorial

Introduction

Here is the source code for com.almende.eve.agent.google.GoogleTaskAgent.java

Source

/*
 * Copyright: Almende B.V. (2014), Rotterdam, The Netherlands
 * License: The Apache Software License, Version 2.0
 */
package com.almende.eve.agent.google;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;

import org.joda.time.DateTime;

import com.almende.eve.agent.Agent;
import com.almende.eve.agent.TaskAgent;
import com.almende.eve.config.Config;
import com.almende.eve.entity.calendar.Authorization;
import com.almende.eve.rpc.annotation.Access;
import com.almende.eve.rpc.annotation.AccessType;
import com.almende.eve.rpc.annotation.Name;
import com.almende.eve.rpc.annotation.Optional;
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.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;

/**
 * The Class GoogleTaskAgent.
 */
@Access(AccessType.PUBLIC)
public class GoogleTaskAgent extends Agent implements TaskAgent {
    private static final Logger LOG = Logger.getLogger(GoogleTaskAgent.class.getName());

    private static final String OAUTH_URI = "https://accounts.google.com/o/oauth2";
    private static final String CALENDAR_URI = "https://www.googleapis.com/tasks/v1/";

    /**
     * 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
     *            the access_token
     * @param token_type
     *            the token_type
     * @param expires_in
     *            the expires_in
     * @param refresh_token
     *            the refresh_token
     * @throws IOException
     *             Signals that an I/O exception has occurred.
     */
    public void setAuthorization(@Name("access_token") final String access_token,
            @Name("token_type") final String token_type, @Name("expires_in") final Integer expires_in,
            @Name("refresh_token") final String refresh_token) throws IOException {
        LOG.info("setAuthorization");

        final State state = getState();

        // retrieve user information
        final String url = "https://www.googleapis.com/oauth2/v1/userinfo";
        final Map<String, String> headers = new HashMap<String, String>();
        headers.put("Authorization", token_type + " " + access_token);
        final String resp = HttpUtil.get(url, headers);

        final ObjectNode info = JOM.getInstance().readValue(resp, ObjectNode.class);
        final String email = info.has("email") ? info.get("email").asText() : null;
        final String name = info.has("name") ? info.get("name").asText() : null;

        final DateTime expires_at = calculateExpiresAt(expires_in);
        final 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(final 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(final Authorization auth) throws Exception {
        final String refresh_token = (auth != null) ? auth.getRefreshToken() : null;
        if (refresh_token == null) {
            throw new Exception("No refresh token available");
        }

        final Config config = getAgentHost().getConfig();
        final String client_id = config.get("google", "client_id");
        final String client_secret = config.get("google", "client_secret");

        // retrieve new access_token using the refresh_token
        final 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");
        final String resp = HttpUtil.postForm(OAUTH_URI + "/token", params);
        final 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")) {
            final Integer expires_in = json.get("expires_in").asInt();
            final DateTime expires_at = calculateExpiresAt(expires_in);
            auth.setExpiresAt(expires_at);
        }
    }

    /**
     * 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 {
        final Authorization auth = getAuthorization();

        final String access_token = (auth != null) ? auth.getAccessToken() : null;
        if (access_token == null) {
            throw new Exception("No authorization token available");
        }
        final String token_type = (auth != null) ? auth.getTokenType() : null;
        if (token_type == null) {
            throw new Exception("No token type available");
        }

        final 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 {
        final Authorization auth = getState().get("auth", Authorization.class);

        // check if access_token is expired
        final DateTime expires_at = (auth != null) ? auth.getExpiresAt() : null;
        if (expires_at != null && expires_at.isBeforeNow()) {
            // TODO: remove this logging
            LOG.info("access token is expired. refreshing now...");
            refreshAuthorization(auth);
            getState().put("auth", auth);
        }

        return auth;
    }

    /**
     * Get the calendar agents version.
     * 
     * @return the description
     */
    @Override
    public String getDescription() {
        return "This agent gives access to a Google Tasks. " + "It allows to search tasks "
                + "and add, edit, or remove tasks.";
    }

    /**
     * Get the calendar agents description.
     * 
     * @return the version
     */
    @Override
    public String getVersion() {
        return "0.1";
    }

    /**
     * Get the username associated with the tasks.
     * 
     * @return name
     */
    @Override
    public String getUsername() {
        return getState().get("name", String.class);
    }

    /**
     * Get the email associated with the tasks.
     * 
     * @return name
     */
    @Override
    public String getEmail() {
        return getState().get("email", String.class);
    }

    /**
     * Retrieve a list with all calendars in this google calendar
     * 
     * @return String with id of the task list
     */
    private String getDefaultTaskList() throws Exception {

        String defaultList = getState().get("defaultList", String.class);
        if (defaultList == null) {
            final ArrayNode taskLists = getTaskList();
            for (final JsonNode taskList : taskLists) {
                if (taskList.get("title").textValue().equals("Paige Task List")) {
                    getState().put("defaultList", taskList.get("id").textValue());
                    return taskList.get("id").textValue();
                }
            }
        }

        // No default list found so going to create one
        if (defaultList == null) {
            ObjectNode taskList = JOM.createObjectNode();
            taskList.put("title", "Paige Task List");
            taskList = createTaskList(taskList);
            defaultList = taskList.get("id").textValue();
            getState().put("defaultList", defaultList);
        }

        return defaultList;
    }

    /**
     * Create a task list.
     * 
     * @param taskList
     *            JSON structure containing a taskList
     * @return JSON sturcture with created tasklist
     * @throws Exception
     *             the exception
     */
    public ObjectNode createTaskList(@Name("taskList") final ObjectNode taskList) throws Exception {

        final String url = CALENDAR_URI + "users/@me/lists";

        // perform POST request
        final ObjectMapper mapper = JOM.getInstance();
        final String body = mapper.writeValueAsString(taskList);
        final Map<String, String> headers = getAuthorizationHeaders();
        headers.put("Content-Type", "application/json");
        final String resp = HttpUtil.post(url, body, headers);
        final ObjectNode createdTaskList = mapper.readValue(resp, ObjectNode.class);

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

        LOG.info("createTaskList=" + JOM.getInstance().writeValueAsString(createdTaskList));

        return createdTaskList;
    }

    /**
     * Retrieve a list with all task lists in this google tasks.
     * 
     * @return the task list
     * @throws Exception
     *             the exception
     */
    @Override
    public ArrayNode getTaskList() throws Exception {
        final String url = CALENDAR_URI + "users/@me/lists";
        final String resp = HttpUtil.get(url, getAuthorizationHeaders());
        final ObjectNode calendars = JOM.getInstance().readValue(resp, ObjectNode.class);

        // check for errors
        if (calendars.has("error")) {
            final 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;
    }

    /**
     * Retrieve a list of task on a certain task list.
     * 
     * @param dueMin
     *            Minimal due time (optional)
     * @param dueMax
     *            Maximal due time (optional)
     * @param taskListId
     *            the task list id
     * @return the tasks
     * @throws Exception
     *             the exception
     */
    @Override
    public ArrayNode getTasks(@Optional @Name("dueMin") final String dueMin,
            @Optional @Name("dueMax") final String dueMax, @Optional @Name("taskListId") String taskListId)
            throws Exception {
        if (taskListId == null) {
            taskListId = getDefaultTaskList();
        }

        if (taskListId == null) {
            throw new Exception("No tasklist given and no default list found");
        }
        // built url with query parameters
        String url = CALENDAR_URI + "lists/" + taskListId + "/tasks";
        final Map<String, String> params = new HashMap<String, String>();
        if (dueMin != null) {
            params.put("dueMin", new DateTime(dueMin).toString());
        }
        if (dueMax != null) {
            params.put("dueMax", new DateTime(dueMax).toString());
        }
        // Set singleEvents=true to expand recurring events into instances
        // params.put("singleEvents", "true");
        url = HttpUtil.appendQueryParams(url, params);

        // perform GET request
        final Map<String, String> headers = getAuthorizationHeaders();
        final String resp = HttpUtil.get(url, headers);
        final ObjectMapper mapper = JOM.getInstance();
        final ObjectNode json = mapper.readValue(resp, ObjectNode.class);

        // check for errors
        if (json.has("error")) {
            final 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");

            /*
             * TODO: cleanup?
             * // 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 a single task by id.
     * 
     * @param taskId
     *            Id of the task
     * @param taskListId
     *            the task list id
     * @return the task
     * @throws Exception
     *             the exception
     */
    @Override
    public ObjectNode getTask(@Name("taskId") final String taskId, @Optional @Name("taskListId") String taskListId)
            throws Exception {
        if (taskListId == null) {
            taskListId = getDefaultTaskList();
        }

        // built url
        final String url = CALENDAR_URI + "lists/" + taskListId + "/tasks/" + taskId;

        // perform GET request
        final Map<String, String> headers = getAuthorizationHeaders();
        final String resp = HttpUtil.get(url, headers);
        final ObjectMapper mapper = JOM.getInstance();
        final ObjectNode task = mapper.readValue(resp, ObjectNode.class);

        LOG.info("getTask task=" + (task != null ? JOM.getInstance().writeValueAsString(task) : null));

        // check for errors
        if (task.has("error")) {
            final ObjectNode error = (ObjectNode) task.get("error");
            final Integer code = error.has("code") ? error.get("code").asInt() : null;
            if (code != null && code.equals(404)) {
                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 (task.has("status") && task.get("status").asText().equals("cancelled")) {
            throw new JSONRPCException(CODE.NOT_FOUND);
        }

        return task;
    }

    /**
     * Create a task.
     * 
     * @param task
     *            JSON structure containing the task
     * @param taskListId
     *            the task list id
     * @return createdTask JSON structure with the created task
     * @throws Exception
     *             the exception
     */
    @Override
    public ObjectNode createTask(@Name("task") final ObjectNode task,
            @Optional @Name("taskListId") String taskListId) throws Exception {
        if (taskListId == null) {
            taskListId = getDefaultTaskList();
        }

        // built url
        final String url = CALENDAR_URI + "lists/" + taskListId + "/tasks";

        // perform POST request
        final ObjectMapper mapper = JOM.getInstance();
        final String body = mapper.writeValueAsString(task);
        final Map<String, String> headers = getAuthorizationHeaders();
        headers.put("Content-Type", "application/json");
        final String resp = HttpUtil.post(url, body, headers);
        final ObjectNode createdTask = mapper.readValue(resp, ObjectNode.class);

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

        LOG.info("createTask=" + JOM.getInstance().writeValueAsString(createdTask));

        return createdTask;
    }

    /**
     * Update an existing task.
     * 
     * @param task
     *            JSON structure containing the task
     *            (task must have an id)
     * @param taskListId
     *            the task list id
     * @return updatedTask JSON structure with the updated task
     * @throws Exception
     *             the exception
     */
    @Override
    public ObjectNode updateTask(@Name("task") final ObjectNode task,
            @Optional @Name("taskListId") String taskListId) throws Exception {
        if (taskListId == null) {
            taskListId = getDefaultTaskList();
        }

        // read id from event
        final String id = task.get("id").asText();
        if (id == null) {
            throw new Exception("Parameter 'id' missing in task");
        }

        // built url
        final String url = CALENDAR_URI + "lists/" + taskListId + "/tasks/" + id;

        // perform POST request
        final ObjectMapper mapper = JOM.getInstance();
        final String body = mapper.writeValueAsString(task);
        final Map<String, String> headers = getAuthorizationHeaders();
        headers.put("Content-Type", "application/json");
        final String resp = HttpUtil.put(url, body, headers);
        final ObjectNode updatedTask = mapper.readValue(resp, ObjectNode.class);

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

        LOG.info("updateTask=" + JOM.getInstance().writeValueAsString(updatedTask)); // TODO:
        // cleanup

        return updatedTask;
    }

    /**
     * Delete an existing task.
     * 
     * @param taskId
     *            id of the task to be deleted
     * @param taskListId
     *            the task list id
     * @throws Exception
     *             the exception
     */
    @Override
    public void deleteTask(@Name("taskId") final String taskId, @Optional @Name("taskListId") String taskListId)
            throws Exception {
        if (taskListId == null) {
            taskListId = getDefaultTaskList();
        }

        LOG.info("deleteTask taskId=" + taskId + ", taskListId=" + taskListId); // TODO:
        // cleanup

        // built url
        final String url = CALENDAR_URI + "lists/" + taskListId + "/tasks/" + taskId;

        // perform POST request
        final Map<String, String> headers = getAuthorizationHeaders();
        final String resp = HttpUtil.delete(url, headers);
        if (!resp.isEmpty()) {
            throw new Exception(resp);
        }
    }

    /**
     * Remove all stored data from this agent.
     */
    public void clear() {
        final State state = getState();
        state.remove("auth");
        state.remove("email");
        state.remove("name");
        state.remove("defaultList");
    }

}