package fi.otavanopisto.muikku.plugins.googlecalendar; import java.io.IOException; import java.security.GeneralSecurityException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import javax.inject.Inject; import org.apache.commons.lang3.StringUtils; import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets; import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets.Details; import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; import com.google.api.client.googleapis.json.GoogleJsonResponseException; import com.google.api.client.http.HttpTransport; import com.google.api.client.http.javanet.NetHttpTransport; import com.google.api.client.json.JsonFactory; import com.google.api.client.json.jackson.JacksonFactory; import com.google.api.client.util.DateTime; import com.google.api.services.calendar.Calendar; import com.google.api.services.calendar.CalendarScopes; import com.google.api.services.calendar.model.AclRule; import com.google.api.services.calendar.model.AclRule.Scope; import com.google.api.services.calendar.model.CalendarListEntry; import com.google.api.services.calendar.model.Event; import com.google.api.services.calendar.model.EventAttendee; import fi.otavanopisto.muikku.calendar.CalendarEvent; import fi.otavanopisto.muikku.calendar.CalendarEventAttendee; import fi.otavanopisto.muikku.calendar.CalendarEventLocation; import fi.otavanopisto.muikku.calendar.CalendarEventReminder; import fi.otavanopisto.muikku.calendar.CalendarEventStatus; import fi.otavanopisto.muikku.calendar.CalendarEventTemporalField; import fi.otavanopisto.muikku.calendar.CalendarEventUser; import fi.otavanopisto.muikku.calendar.CalendarServiceException; import fi.otavanopisto.muikku.calendar.DefaultCalendarEvent; import fi.otavanopisto.muikku.calendar.DefaultCalendarEventLocation; import fi.otavanopisto.muikku.plugins.googlecalendar.model.GoogleCalendar; import fi.otavanopisto.muikku.plugins.googlecalendar.model.GoogleCalendarEventUser; import fi.otavanopisto.muikku.session.AccessToken; import fi.otavanopisto.muikku.session.SessionController; public class GoogleCalendarClient { private static final HttpTransport TRANSPORT = new NetHttpTransport(); private static final JsonFactory JSON_FACTORY = new JacksonFactory(); @Inject private Logger logger; @Inject private SessionController sessionController; public fi.otavanopisto.muikku.calendar.Calendar createCalendar(String summary, String description) throws CalendarServiceException { com.google.api.services.calendar.model.Calendar calendar = new com.google.api.services.calendar.model.Calendar(); calendar.setSummary(summary); calendar.setDescription(description); try { calendar = getClient().calendars().insert(calendar).execute(); } catch (IOException | GeneralSecurityException ex) { throw new CalendarServiceException(ex); } return new GoogleCalendar(summary, description, calendar.getId(), true); } public fi.otavanopisto.muikku.calendar.Calendar findCalendar(String id) throws CalendarServiceException { try { com.google.api.services.calendar.model.Calendar result = getClient().calendars().get(id).execute(); return new GoogleCalendar(result.getSummary(), result.getDescription(), id, isWritable(result)); } catch (GoogleJsonResponseException ex) { return null; } catch (IOException | GeneralSecurityException ex) { throw new CalendarServiceException(ex); } } public List<fi.otavanopisto.muikku.calendar.Calendar> listPublicCalendars() throws CalendarServiceException { try { Calendar client = getClient(); ArrayList<fi.otavanopisto.muikku.calendar.Calendar> result = new ArrayList<>(); for (CalendarListEntry entry : client.calendarList().list().execute().getItems()) { result.add( new GoogleCalendar(entry.getSummary(), entry.getDescription(), entry.getId(), isWritable(entry))); } return result; } catch (IOException | GeneralSecurityException ex) { throw new CalendarServiceException(ex); } } public fi.otavanopisto.muikku.calendar.Calendar updateCalendar(fi.otavanopisto.muikku.calendar.Calendar calendar) throws CalendarServiceException { try { getClient().calendars().update( calendar.getId(), new com.google.api.services.calendar.model.Calendar() .setDescription(calendar.getDescription()) .setSummary(calendar.getDescription())); return calendar; } catch (GeneralSecurityException | IOException ex) { throw new CalendarServiceException(ex); } } public void deleteCalendar(fi.otavanopisto.muikku.calendar.Calendar calendar) throws CalendarServiceException { try { getClient().calendars().delete(calendar.getId()); } catch (GeneralSecurityException | IOException ex) { throw new CalendarServiceException(ex); } } public AclRule insertCalendarAclRule(String calendarId, AclRule aclRule) throws CalendarServiceException { try { return getClient().acl().insert(calendarId, aclRule).execute(); } catch (IOException | GeneralSecurityException ex) { throw new CalendarServiceException(ex); } } public AclRule insertCalendarUserAclRule(String calendarId, String email, String role) throws CalendarServiceException { Scope scope = new Scope(); scope.setType("user"); scope.setValue(email); AclRule aclRule = new AclRule(); aclRule.setRole(role); aclRule.setScope(scope); return insertCalendarAclRule(calendarId, aclRule); } public CalendarEvent createEvent( String calendarId, String summary, String description, CalendarEventStatus status, List<CalendarEventAttendee> attendees, CalendarEventTemporalField start, CalendarEventTemporalField end, String recurrence, boolean allDay) throws CalendarServiceException { ArrayList<EventAttendee> googleAttendees = new ArrayList<>(); for (CalendarEventAttendee attendee : attendees) { googleAttendees.add( new EventAttendee() .setDisplayName(attendee.getDisplayName()) .setComment(attendee.getDisplayName()) .setEmail(attendee.getEmail()) .setResponseStatus(attendee.getStatus().toString().toLowerCase(Locale.ROOT)) ); } try { Event event = new Event() .setSummary(summary) .setDescription(description) .setStatus(status.toString().toLowerCase(Locale.ROOT)) .setAttendees(googleAttendees) .setStart(Convert.toEventDateTime(allDay, start)) .setEnd(Convert.toEventDateTime(allDay, end)); if (StringUtils.isNotBlank(recurrence)) { event.setRecurrence(Arrays.asList(String.format("RRULE:%s", recurrence))); } return toMuikkuEvent(calendarId, getClient().events().insert(calendarId, event).execute()); } catch (IOException | GeneralSecurityException ex) { throw new CalendarServiceException(ex); } } public CalendarEvent findEvent(fi.otavanopisto.muikku.calendar.Calendar calendar, String eventId) throws CalendarServiceException { try { Event event = getClient().events().get(calendar.getId(), eventId).execute(); return toMuikkuEvent(calendar.getId(), event); } catch (GeneralSecurityException | IOException ex) { throw new CalendarServiceException(ex); } } public List<CalendarEvent> listEvents(String... calendarId) throws CalendarServiceException { ArrayList<CalendarEvent> result = new ArrayList<>(); for (String calId : calendarId) { try { for (Event event : getClient().events().list(calId).execute().getItems()) { result.add(toMuikkuEvent(calId, event)); } } catch (GeneralSecurityException | IOException ex) { throw new CalendarServiceException(ex); } } return result; } public List<CalendarEvent> listEvents(java.time.OffsetDateTime minTime, java.time.OffsetDateTime maxTime, String... calendarId) throws CalendarServiceException { ArrayList<CalendarEvent> result = new ArrayList<>(); for (String calId : calendarId) { try { for (Event event : getClient() .events() .list(calId) .setTimeMin(minTime != null ? new DateTime(minTime.toInstant().toEpochMilli()) : null) .setTimeMax(maxTime != null ? new DateTime(maxTime.toInstant().toEpochMilli()) : null) .execute() .getItems()) { result.add(toMuikkuEvent(calId, event)); logger.log(Level.INFO, event.toPrettyString()); } } catch (GeneralSecurityException | IOException ex) { throw new CalendarServiceException(ex); } } return result; } public CalendarEvent updateEvent(CalendarEvent calendarEvent) throws CalendarServiceException { ArrayList<EventAttendee> googleAttendees = new ArrayList<>(); for (CalendarEventAttendee attendee : calendarEvent.getAttendees()) { googleAttendees.add( new EventAttendee() .setDisplayName(attendee.getDisplayName()) .setComment(attendee.getDisplayName()) .setEmail(attendee.getEmail()) .setResponseStatus(attendee.getStatus().toString().toLowerCase(Locale.ROOT)) ); } try { Event event = new Event() .setSummary(calendarEvent.getSummary()) .setDescription(calendarEvent.getDescription()) .setStatus(calendarEvent.getStatus().toString().toLowerCase(Locale.ROOT)) .setAttendees(googleAttendees) .setStart(Convert.toEventDateTime(calendarEvent.isAllDay(), calendarEvent.getStart())) .setEnd(Convert.toEventDateTime(calendarEvent.isAllDay(), calendarEvent.getEnd())); if (StringUtils.isNotBlank(calendarEvent.getRecurrence())) { event.setRecurrence(Arrays.asList(String.format("RRULE:%s", calendarEvent.getRecurrence()))); } return toMuikkuEvent(calendarEvent.getCalendarId(), getClient().events().patch(calendarEvent.getCalendarId(), calendarEvent.getId(), event).execute()); } catch (IOException | GeneralSecurityException ex) { throw new CalendarServiceException(ex); } } private CalendarEvent toMuikkuEvent(String calendarId, Event event) { String url = event.getHangoutLink(); CalendarEventLocation location = new DefaultCalendarEventLocation(event.getLocation(), null, null, null); CalendarEventStatus status = CalendarEventStatus.valueOf(event.getStatus().toUpperCase(Locale.ROOT)); // TODO: attendees List<CalendarEventAttendee> attendees = null; CalendarEventUser organizer = new GoogleCalendarEventUser(event.getOrganizer().getDisplayName(), event.getOrganizer().getEmail()); CalendarEventTemporalField start = Convert.toCalendarEventTemporalField(event.getStart()); CalendarEventTemporalField end = Convert.toCalendarEventTemporalField(event.getEnd()); boolean allDay = event.getStart().getDate() != null; Map<String, String> extendedProperties = Collections.emptyMap(); // TODO: reminders List<CalendarEventReminder> reminders = null; String recurrence = null; if (event.getRecurrence() != null && !event.getRecurrence().isEmpty()) { List<String> rrules = new ArrayList<String>(); for (String rule : event.getRecurrence()) { if (StringUtils.startsWith(rule, "RRULE:")) { rrules.add(rule); } else { logger.warning(String.format("Ignoring unsupported recurrence rule %s from Google", rule)); } } if (rrules.isEmpty()) { logger.warning("Could not parse recurring event recurrene because all rules were in unsupported formats"); } else { if (rrules.size() > 1) { logger.warning(String.format("More than one recurrence rule defined. Ignoring %d rules", rrules.size() - 1)); } recurrence = StringUtils.substring(rrules.get(0), 6); } } return new DefaultCalendarEvent(event.getId(), calendarId, "google", event.getSummary(), event.getDescription(), url, location, status, attendees, organizer, start, end, allDay, Convert.toDate(event.getCreated()), Convert.toDate(event.getUpdated()), extendedProperties, reminders, recurrence); } public void deleteEvent(fi.otavanopisto.muikku.calendar.Calendar calendar, String eventId) throws CalendarServiceException { try { getClient().events().delete(calendar.getId(), eventId).execute(); } catch (GeneralSecurityException | IOException ex) { throw new CalendarServiceException(ex); } } private boolean isWritable(CalendarListEntry entry) { if ("reader".equals(entry.getAccessRole()) || "freeBusyReader".equals(entry.getAccessRole())) { return false; } else { return true; } } private boolean isWritable(com.google.api.services.calendar.model.Calendar cal) { return true; // TODO } private Calendar getClient() throws GeneralSecurityException, IOException { return new Calendar.Builder(TRANSPORT, JSON_FACTORY, getServiceAccountCredential()) .setApplicationName("Muikku") .build(); } private GoogleCredential getServiceAccountCredential() throws GeneralSecurityException, IOException { String accountEmail = System.getProperty("muikku.googleServiceAccount.accountEmail"); if (StringUtils.isBlank(accountEmail)) { throw new GeneralSecurityException("muikku.googleServiceAccount.accountEmail environment property is missing"); } String accountUser = System.getProperty("muikku.googleServiceAccount.accountUser"); if (StringUtils.isBlank(accountUser)) { throw new GeneralSecurityException("muikku.googleServiceAccount.accountUser environment property is missing"); } String keyFilePath = System.getProperty("muikku.googleServiceAccount.keyFile"); if (StringUtils.isBlank(keyFilePath)) { throw new GeneralSecurityException("muikku.googleServiceAccount.keyFile environment property is missing"); } java.io.File keyFile = new java.io.File(keyFilePath); if (!keyFile.exists()) { throw new GeneralSecurityException("muikku.googleServiceAccount.keyFile environment property is pointing into non-existing file"); } return new GoogleCredential.Builder() .setTransport(new NetHttpTransport()) .setJsonFactory(new JacksonFactory()) .setServiceAccountId(accountEmail) .setServiceAccountScopes(Arrays.asList(CalendarScopes.CALENDAR)) .setServiceAccountPrivateKeyFromP12File(keyFile) .setServiceAccountUser(accountUser) .build(); } @SuppressWarnings("unused") private GoogleCredential getAccessTokenCredential() { AccessToken googleAccessToken = sessionController.getOAuthAccessToken("google"); if (googleAccessToken != null) { Details details = new Details(); details.setClientId("-"); details.setClientSecret("-"); GoogleClientSecrets secrets = new GoogleClientSecrets(); secrets.setWeb(details); return new GoogleCredential.Builder() .setClientSecrets(secrets) .setTransport(TRANSPORT) .setJsonFactory(JSON_FACTORY) .build() .setAccessToken(googleAccessToken.getToken()); } return null; } }