Гуглдарь-батюшка

Posted on: July 24th, 2011 by Spade No Comments »


Google Calendar – полезный (так думают многие) и удобный (этих уже поменьше) сервис от корпорации добра, позволяющий составлять расписания с заметками и оповещениями для повседневных дел, бизнес встреч, тур-поездок и т.д. – кто на что горазд. Как обычно, все, что нужно – аккаунт гугл. Залогинились – и видим настраиваемый интерфейс и список наших планов в нем. Все крутится вокруг событий – главный объект, имеющий даты начала-конца, имя и описание. Событие может быть повторяющимся – например, чистка зубов каждый день (а у кого мама была врачом – те и дважды в день). Календарь можно открывать для просмотра своим знакомым – указывая их гугл аккаунт. Уровень доступа другого аккаунта может варьироваться – от «только чтение», до «вносить изменения и предоставлять доступ». Вот в общем и все – календарь прост в обращении и интуитивно понятен.

Теперь представим ситуацию, что вам нужно встроить в своё приложение поддержку гугл календаря. Допустим, мы занимаемся доставкой заказов, и хотим в календаре держать расписание этой карусели. Создадим для этого библиотечку  (маленькую такую…).

У нас будет 5 классов:

  • CalendarManager
  • CalendarEvent
  • EventQueryParams
  • InvalidQueryParamsException
  • CalendarTestCase

Первый – комбайн содержащий в себе весь функционал (запросы типа «получить, добавить, удалить, изменить» для событий). Второй – POJO событие календаря. Третье – объект с параметрами по определенным полям события. Потом – исключение, и тесткейс.

Начнем с ядра: событие.

public class CalendarEvent implements Serializable{
 
    private String id;
    private Long orderID;
    private String title;
    private String description;
    private Date startTime;
    private Date endTime;
 
    public CalendarEvent() {
    }
 
    public CalendarEvent(String id, String title, String description, Date startTime, Date endTime) {
        this.id = id;
        this.title = title;
        this.description = description;
        this.startTime = startTime;
        this.endTime = endTime;
    }
 
    public String getId() {
        return id;
    }
 
    public CalendarEvent setId(String id) {
        this.id = id;
        return this;
    }
//other code....
}

Простой объект – для удобства конструктор с нужными полями. Все setter методы возвращают ссылку на объект для реализации сцепленных вызовов (Builder pattern 8)). Обратите внимание на поле orderID – это идентификатор нашего заказа в бд, и его мы сохраняем отдельным полем в календаре. Так как других способов отследить уникальность нет – есть возможность создавать несколько событий с одним именем, временем и описанием.

public class EventQueryParams implements Serializable {
 
    private String searchText;
    private Long orderID;
    private Date minTime;
    private Date maxTime;
    private Integer from;
    private Integer maxResults;
 
    public EventQueryParams() {
    }
 
    public EventQueryParams(Long orderID) {
        this.orderID = orderID;
    }
 
    public EventQueryParams(Date minTime, Date maxTime) {
        this.minTime = minTime;
        this.maxTime = maxTime;
    }
 
    public String getSearchText() {
        return searchText;
    }
 
    public EventQueryParams setSearchText(String searchText) {
        this.searchText = searchText;
        return this;
    }
//other code...
}

EventQueryParams – тот же принцип построения. Содержит в себе настройки для поиска.

Типы поиска особо разбежаться не дадут – API дает доступ к startTime, endTime, и полнотекстовый поиск – то есть искать отдельно по имени или описанию нельзя. Можно просто передать текст и ожидать, что «оно вернет что надо» – однако нас будет ждать разочарование… Например даже имея событие с уникальным названием и передавая его в поиск – он может ничего не вернуть. Очевидно там есть свои особые алгоритмы – как и в основном движке поиска гугла… Тут на спасение приходят ExtendedProperty – особые параметры события, с которыми можно обращаться, используя API (в основном интерфейсе они не будут нигде видны). Так мы сможем устанавливать orderID и искать по его точному значению – тут все будет предсказуемо. Ах, чуть не забыл – выборку можно ограничить – с помощью задания стартового индекса возвращаемой записи и количества, которое нужно вернуть (аналог limit MySQL).

Переходим к нашему комбайну…

    public CalendarManager(String calendarName, String credentialName, String credentialPass) throws Exception {
        this.credentialName = credentialName;
        this.credentialPass = credentialPass;
 
        configProperties = new Properties();
        configProperties.load(getClass().getClassLoader().getResourceAsStream("calendar.properties"));
 
        applicationName = (String) configProperties.get("app.name");
        gmtVal = (String) configProperties.get("gmt");
 
        tz = TimeZone.getTimeZone("GMT" + gmtVal);
 
        feedUrl = new URL(new StringBuilder("https://www.google.com/calendar/feeds/").append(calendarName).append("/private/full").toString());
        service = new CalendarService(applicationName);
        service.setUserCredentials(credentialName, credentialPass);
    }

Constructor. User Credentials  – достать на гугле. Специфика области – работа с TimeZone – её нужно выбрать правильно (под пользователей вашего сервиса), иначе есть вероятность, что они пропустят все встречи J. В файле .properties мы содержим имя приложения (гугл просит, чтоб мы его как-то назвали) и значение GMT – для нас это будет +03:00. Урл календаря создается как https://www.google.com/calendar/feeds/{calendar_name}/private/full – обратите внимание, что имя календаря соответствует тому, с которым мы собираемся работать, а это не обязательно наш собственный – это может быть календарь другого гая, к которому он нам дал доступ по доброте сердечной…

Функция поиска. Из интересных возможностей – сортировка по полю и порядок сортировки. Остальное в общем понятно.

    public List getEvents(EventQueryParams params) {
 
        List events = new ArrayList();
 
        try {
 
            CalendarQuery query = new CalendarQuery(feedUrl);
 
            query.addCustomParameter(new Query.CustomParameter("orderby", "starttime"));
            query.addCustomParameter(new Query.CustomParameter("sortorder", "descending"));
 
            if (params.getSearchText() != null) {
                query.setFullTextQuery(params.getSearchText());
            }
 
            if (params.getOrderID() != null) {
                CalendarQuery.ExtendedPropertyMatch match = new CalendarQuery.ExtendedPropertyMatch(ORDER_ID, params.getOrderID().toString());
                query.setExtendedPropertyQuery(match);
            }
 
            if (params.getMinTime() != null) {
                query.setMinimumStartTime(new DateTime(params.getMinTime(), tz));
            }
 
            if (params.getMaxTime() != null) {
                query.setMaximumStartTime(new DateTime(params.getMaxTime(), tz));
            }
 
            if (params.getMaxResults() != null) {
                query.setMaxResults(params.getMaxResults());
            }
 
            if (params.getFrom() != null) {
                query.setStartIndex(params.getFrom());
            }
 
            CalendarEventFeed feed = service.query(query, CalendarEventFeed.class);
 
            for(CalendarEventEntry entry: feed.getEntries()) {
 
                CalendarEvent calendarEvent = new CalendarEvent(entry.getId(), entry.getTitle().getPlainText(), entry.getPlainTextContent(),
                        new Date(entry.getTimes().get(0).getStartTime().getValue()),
                        new Date(entry.getTimes().get(0).getEndTime().getValue()));
 
                for (ExtendedProperty property: entry.getExtendedProperty()) {
                    if (property.getName().equals(ORDER_ID)) {
                        try {
                            calendarEvent.setOrderID(Long.parseLong(property.getValue()));
                        } catch (NumberFormatException e) {
                            log.warn("Failed to parse order id from calendar event.", e);
                        }
                    }
                }
 
                events.add(calendarEvent);
            }
        } catch (Exception e) {
            log.error("Error getting calendar events.", e);
        }
        return events;
    }

Добавление события. Объект PlainTextConstruct используем для установки названия и описания события. Объект When – для установки начала и конца события. Зачем-то у события это свойство в виде коллекции (возможно для повторяющихся событий – не было времени выяснить, оставляю на самостоятельную работу всем желающим). Этот объект мы просто добавляем в список.

    public Boolean addEvent(CalendarEvent event) {
 
        Boolean result = false;
 
        try {
 
            CalendarEventEntry entry = new CalendarEventEntry();
 
            entry.setTitle(new PlainTextConstruct(event.getTitle()));
            entry.setContent(new PlainTextConstruct(event.getDescription()));
 
            When eventTimes = new When();
            eventTimes.setStartTime(new DateTime(event.getStartTime(), tz));
            eventTimes.setEndTime(new DateTime(event.getEndTime(), tz));
            entry.addTime(eventTimes);
 
            if (event.getOrderID() != null) {
                ExtendedProperty property = new ExtendedProperty();
                property.setName(ORDER_ID);
                property.setValue(event.getOrderID().toString());
                entry.addExtension(property);
            }
 
            service.insert(feedUrl, entry);
 
            result = true;
        } catch (Exception e) {
            log.error("Error creating calendar events.", e);
        }
 
        return result;
    }

Функцию изменения события постить не буду – и так уже много кода. Вставим только удаление и поиск по нашему параметру.

    public Boolean deleteEvent(EventQueryParams params) {
 
        Boolean result = false;
 
        try {
 
            CalendarEventEntry entry = findEntry(params);
 
            entry.delete();
 
            result = true;
        } catch (Exception e) {
            log.error("Error deleting calendar events.", e);
        }
 
        return result;
    }
 
    private CalendarEventEntry findEntry(EventQueryParams params) throws IOException, ServiceException {
 
        CalendarQuery query = new CalendarQuery(feedUrl);
 
        if (params.getOrderID() == null) {
            throw new InvalidQueryParamsException(InvalidQueryParamsException.ORDER_ID_NOT_SET);
        }
        CalendarQuery.ExtendedPropertyMatch match = new CalendarQuery.ExtendedPropertyMatch(ORDER_ID, params.getOrderID().toString());
        query.setExtendedPropertyQuery(match);
 
        CalendarEventFeed feed = service.query(query, CalendarEventFeed.class);
 
        if (feed.getEntries().isEmpty()) {
            throw new InvalidQueryParamsException(InvalidQueryParamsException.EVENT_NOT_FOUND);
        }
 
        return feed.getEntries().get(0);
    }

Если читатели изъявят желание – выложу код библиотеки аргивчегом… У нас предстоит еще две статьи касательно библиотек. В заключении (хотя правильно было бы в начале) – опишу какую структуру папок и файлов можно (но не обязательно) использовать для удобства разработки и сборки библиотеки. А в следующей статье мы напишем либу для работы с (угадайте по девизу): Share, manage and access all your business content online…

Leave a Reply