/* Copyright (c) 2008 Google Inc.
*
* 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.
*/
package mashups.eventpub;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.GeneralSecurityException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.LinkedList;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Date;
import com.google.api.gbase.client.DateTimeRange;
import com.google.api.gbase.client.FeedURLFactory;
import com.google.api.gbase.client.GoogleBaseAttributesExtension;
import com.google.api.gbase.client.GoogleBaseEntry;
import com.google.api.gbase.client.GoogleBaseService;
import com.google.gdata.util.ServiceException;
import com.google.gdata.client.spreadsheet.SpreadsheetService;
import com.google.gdata.client.calendar.CalendarService;
import com.google.gdata.client.http.AuthSubUtil;
import com.google.gdata.data.DateTime;
import com.google.gdata.data.PlainTextConstruct;
import com.google.gdata.data.calendar.CalendarEventEntry;
import com.google.gdata.data.extensions.When;
import com.google.gdata.data.spreadsheet.CellEntry;
import com.google.gdata.data.spreadsheet.CellFeed;
import com.google.gdata.data.spreadsheet.CustomElementCollection;
import com.google.gdata.data.spreadsheet.ListEntry;
import com.google.gdata.data.spreadsheet.ListFeed;
import com.google.gdata.data.spreadsheet.SpreadsheetEntry;
import com.google.gdata.data.spreadsheet.SpreadsheetFeed;
import com.google.gdata.data.spreadsheet.WorksheetEntry;
import com.google.gdata.data.spreadsheet.WorksheetFeed;
import com.google.gdata.util.httputil.FastURLEncoder;
/**
* Publisher for pushing events from Spreadsheets to Calendar and Base
*
*
*/
public class EventPublisher {
/**
* The URL for the Spreadsheets meta feed, containing entries representing
* individual spreadsheets accessible to the authenticated user
*/
private static final String SPREADSHEETS_META_FEED =
"http://spreadsheets.google.com/feeds/spreadsheets/private/full";
/**
* The URL for the Spreadsheets feed scope, as passed to the AuthSub
* service in the AuthSubRequest URL scope parameter.
*/
private static final String SPREADSHEETS_SCOPE =
"http://spreadsheets.google.com/feeds/";
/**
* The app identity string used for the 'source' required by the
* ClientLogin service. The GData Java Client Library also sends this value
* to the GData services as part of the <code>User-Agent</code> HTTP header.
*/
private static final String APP_IDENTITY = "google-mashups-EventPublisher";
/**
* The authenticated CalendarService object used for publishing
*/
private CalendarService calService = null;
/**
* The authenticated CalendarService object used for publishing
*/
private GoogleBaseService baseService = null;
/**
* The authenticated SpreadsheetService object used for retrieving events
*/
private SpreadsheetService ssService = null;
/**
* URL representing the Google Spreadsheets list feed from which the events
* are published. Note, this feed is not the meta feed of spreadsheets. It
* is the list feed containing entries representing each row of the
* spreadsheet.
*/
private URL ssUrl = null;
/* Spreadsheets credentials for authentication */
private String ssUsername = null;
private String ssPassword = null;
private String ssAuthSubToken = null;
/**
* URL representing the Google Calendar feed to which events can be
* published
*/
private URL calUrl = null;
/* Calendar credentials for authentication */
private String calUsername = null;
private String calPassword = null;
/* Google Base credentials for authentication */
private String baseUsername = null;
private String basePassword = null;
/**
* Contains the mapping between field names used by the
* spreadsheet and those required in Calendar and Base
*/
private SpreadsheetCustomFieldMap fieldMap = null;
public void setFieldMap(SpreadsheetCustomFieldMap fieldMap) {
this.fieldMap = fieldMap;
}
public void setSsUrl(String ssUrlText) throws MalformedURLException {
this.ssUrl = new URL(ssUrlText);
}
public void setSsUsernamePassword(String username, String password) {
this.ssUsername = username;
this.ssPassword = password;
}
public void setCalUsernamePassword(String username, String password) {
this.calUsername = username;
this.calPassword = password;
}
public void setBaseUsernamePassword(String username, String password) {
this.baseUsername = username;
this.basePassword = password;
}
public void setCalUrl(String calUrl) throws MalformedURLException {
this.calUrl = new URL(calUrl);
}
public String getSsAuthSubToken() {
return this.ssAuthSubToken;
}
/**
* Sets the AuthSub token used for Google Spreadsheets data API auth.
* The method can, optionally, exchange the token for a session token
* should a session token be desired.
*
* @param token The AuthSub token (either a a single-use or session token)
* @param exchange True if the token supplied is a single-use token and
* should be exchanged for a session token.
* @throws EPAuthenticationException
*/
public void setSsAuthSubToken(String token, boolean exchange)
throws EPAuthenticationException {
if (exchange) {
try {
this.ssAuthSubToken = AuthSubUtil.exchangeForSessionToken(token, null);
} catch (com.google.gdata.util.AuthenticationException authEx) {
throw new
EPAuthenticationException(
"Single use token could not be exchanged", authEx);
} catch (IOException ex) {
throw new
EPAuthenticationException(
"Single use token could not be exchanged", ex);
} catch (GeneralSecurityException ex) {
throw new
EPAuthenticationException(
"Single use token could not be exchanged", ex);
}
} else {
this.ssAuthSubToken = token;
}
}
/**
* Retrieves events from the spreadsheet and creates a <code>List</code>
* of <code>Event</code> instances representing the events.
*
* @throws EPAuthenticatonException
*/
public List<Event> getEventsFromSpreadsheet()
throws EPAuthenticationException {
List<Event> eventList = new LinkedList<Event>();
List<ListEntry> ssEntryList = getSsEntryListHelper();
// counter used to store row number in event
int i = 1;
for (ListEntry ssRow : ssEntryList) {
i++;
// CustomElementCollection represents elements in the gsx namespace
CustomElementCollection elements = ssRow.getCustomElements();
try {
Event e = new Event(
elements.getValue(fieldMap.getTitleColumn()),
elements.getValue(fieldMap.getDescriptionColumn()),
elements.getValue(fieldMap.getWebsiteColumn()),
elements.getValue(fieldMap.getStartColumn()),
elements.getValue(fieldMap.getEndColumn()),
elements.getValue(fieldMap.getCalendarUrlColumn()),
elements.getValue(fieldMap.getBaseUrlColumn()));
e.ssRow = i;
e.setSsEditUrl(ssRow.getEditLink().getHref());
eventList.add(e);
} catch (MalformedURLException urlEx) {
System.err.println("Could not read event titled '" +
elements.getValue(fieldMap.getTitleColumn()) +
"' due to a bad URL for the Calendar or Base URL");
}
}
return eventList;
}
/**
* Retrieves a list of Spreadsheets for the authenticated user
*
* @return Returns a list of HashMaps with meta-data about each spreadsheet.
* @throws EPAuthenticationException
*/
public List<HashMap> getSsList()
throws EPAuthenticationException {
List<HashMap> returnList = new LinkedList<HashMap>();
List<SpreadsheetEntry> ssList = getSsListHelper();
for (SpreadsheetEntry ssEntry : ssList) {
HashMap<String, String> hm = new HashMap<String, String>();
hm.put("title", ssEntry.getTitle().getPlainText());
try {
hm.put("wsFeed", FastURLEncoder.encode(
ssEntry.getWorksheetFeedUrl().toString(),
"UTF-8"));
} catch (UnsupportedEncodingException e) {
System.err.println("Encoding error: " + e.getMessage());
}
returnList.add(hm);
}
return returnList;
}
/**
* Retrieves a list of Worksheets for the specified
* spreadsheet
*
* @param wsFeedUrl The URL for the feed containing the list of worksheets
* @return A <code>List</code> of <code>HashMap</code> meta data about each
* worksheet
* @throws EPAuthenticationException
*/
public List<HashMap> getWsList(String wsFeedUrl)
throws EPAuthenticationException {
List<HashMap> returnList = new LinkedList<HashMap>();
List<WorksheetEntry> wsList = getWsListHelper(wsFeedUrl);
for (WorksheetEntry wsEntry : wsList) {
HashMap<String, String> hm = new HashMap<String, String>();
hm.put("title", wsEntry.getTitle().getPlainText());
try {
hm.put("cellFeed", FastURLEncoder.encode(
wsEntry.getCellFeedUrl().toString(),
"UTF-8"));
} catch (UnsupportedEncodingException e) {
System.err.println("Encoding error: " + e.getMessage());
}
returnList.add(hm);
}
return returnList;
}
/**
* Returns the URL for redirecting users to in order to authenticate to
* the AuthSub service for access to a Google Spreadsheets account via
* the API.
*
* @param nextUrl The URL to which the authenticated user will be redirected
* to after successfully authenticating
*/
public static String getSsAuthSubUrl(String nextUrl) {
// requests a single-use token which can be upgraded to a session token
// and is not set with the AuthSub secure flag
return AuthSubUtil.getRequestUrl(nextUrl,
SPREADSHEETS_SCOPE,
false,
true);
}
/**
* Returns a <code>SpreadsheetService</code> object representing the
* set APP_IDENTITY and credentials. If both an AuthSub token and
* ClientLogin (username/password) credentials are set, the AuthSub
* token takes precedence for authentication
*
* @return an authentication <code>SpreadsheetService</code> instance
* @throws EPAuthenticationException
*/
private SpreadsheetService getSsService()
throws EPAuthenticationException {
if (this.ssService == null) {
SpreadsheetService ssService = new SpreadsheetService(APP_IDENTITY);
try {
if (ssAuthSubToken != null) {
ssService.setAuthSubToken(ssAuthSubToken);
} else if (ssUsername != null && ssPassword != null) {
ssService.setUserCredentials(ssUsername, ssPassword);
}
} catch (com.google.gdata.util.AuthenticationException authEx) {
throw new EPAuthenticationException("Bad spreadsheets credentials");
}
this.ssService = ssService;
return ssService;
} else {
return this.ssService;
}
}
/**
* Returns a <code>CalendarService</code> object representing the
* set APP_IDENTITY and credentials. If both an AuthSub token and
* ClientLogin (username/password) credentials are set, the AuthSub
* token takes precedence for authentication
*
* @return an authentication <code>CalendarService</code> instance
* @throws EPAuthenticationException
*/
private CalendarService getCalService()
throws EPAuthenticationException {
if (this.calService == null) {
CalendarService calService = new CalendarService(APP_IDENTITY);
try {
calService.setUserCredentials(calUsername, calPassword);
} catch (com.google.gdata.util.AuthenticationException authEx) {
throw new EPAuthenticationException("Bad calendar credentials");
}
this.calService = calService;
return calService;
} else {
return this.calService;
}
}
/**
* Returns a <code>BaseService</code> object representing the
* set APP_IDENTITY and credentials. If both an AuthSub token and
* ClientLogin (username/password) credentials are set, the AuthSub
* token takes precedence for authentication
*
* @return an authentication <code>BaseService</code> instance
* @throws EPAuthenticationException
*/
private GoogleBaseService getBaseService()
throws EPAuthenticationException {
if (this.baseService == null) {
GoogleBaseService baseService = new GoogleBaseService(APP_IDENTITY,"none");
try {
baseService.setUserCredentials(baseUsername, basePassword);
} catch (com.google.gdata.util.AuthenticationException authEx) {
throw new EPAuthenticationException("Bad calendar credentials");
}
this.baseService = baseService;
return baseService;
} else {
return this.baseService;
}
}
/**
* Takes the event objects passed and publishes each of them to Google Base
*
* @param eventList The list of <code>Event</code>s to publish
*/
public void publishEventsToBase(List<Event> eventList) {
Iterator<Event> i = eventList.iterator();
while (i.hasNext()) {
Event event = i.next();
try {
publishEventToBase(event);
} catch (EPAuthenticationException e) {
System.err.println("Authentication problem when publishing events: " +
e.getMessage());
} catch (IOException e) {
System.err.println("IOException when publishing events: " +
e.getMessage());
} catch (ServiceException e) {
e.printStackTrace();
System.err.println("ServiceException when publishing events: " +
e.getMessage());
}
}
}
/**
* Takes the event objects passed and publishes each of them to Calendar
*
* @param eventList The list of <code>Event</code>s to publish
*/
public void publishEventsToCalendar(List<Event> eventList) {
Iterator<Event> i = eventList.iterator();
while (i.hasNext()) {
Event event = i.next();
try {
publishEventToCalendar(event);
} catch (EPAuthenticationException e) {
System.err.println("Authentication problem when publishing events: " +
e.getMessage());
} catch (IOException e) {
System.err.println("IOException when publishing events: " +
e.getMessage());
} catch (ServiceException e) {
e.printStackTrace();
System.err.println("ServiceException when publishing events: " +
e.getMessage());
}
}
}
/**
* Publishes an individual event to Calendar
*
* @param event The <code>Event</code> to publish
* @throws EPAuthenticationException
* @throws IOException
* @throws ServiceException
*/
private void publishEventToCalendar(Event event)
throws EPAuthenticationException, IOException, ServiceException {
CalendarService calService = getCalService();
CalendarEventEntry entry = null;
if (event.getCalendarUrl() != null) {
// updating event
entry = calService.getEntry(event.getCalendarUrl(),
CalendarEventEntry.class);
} else {
// publishing new event
entry = new CalendarEventEntry();
}
// set data on event
entry.setTitle(new PlainTextConstruct(event.getTitle()));
entry.setContent(new PlainTextConstruct(event.getDescription()));
When when = new When();
DateTime startDateTime = new DateTime(event.getStartDate());
startDateTime.setDateOnly(true);
// we must add 1 day to the event as the end-date is exclusive
Calendar endDateCal = new GregorianCalendar();
endDateCal.setTime(event.getEndDate());
endDateCal.add(Calendar.DATE, 1);
DateTime endDateTime = new DateTime(endDateCal.getTime());
endDateTime.setDateOnly(true);
when.setStartTime(startDateTime);
when.setEndTime(endDateTime);
entry.getTimes().add(when);
if (event.getCalendarUrl() != null) {
// updating event
entry.update();
} else {
// insert event
CalendarEventEntry resultEntry = calService.insert(calUrl, entry);
updateSsEventEditUrl(event.getSsEditUrl(),
resultEntry.getEditLink().getHref(),
null);
}
}
/**
* Publishes an individual event to Base
*
* @param event The <code>Event</code> to publish
* @throws EPAuthenticationException
* @throws IOException
* @throws ServiceException
*/
private void publishEventToBase(Event event)
throws EPAuthenticationException, IOException, ServiceException {
GoogleBaseService baseService = getBaseService();
GoogleBaseEntry entry = null;
if (event.getBaseUrl() != null) {
// updating base entry
entry = baseService.getEntry(event.getBaseUrl(),
GoogleBaseEntry.class);
entry = new GoogleBaseEntry();
} else {
// publishing new entry
entry = new GoogleBaseEntry();
}
// prepare an 'events and activities' item for publishing
GoogleBaseAttributesExtension gbaseAttributes =
entry.getGoogleBaseAttributes();
entry.setTitle(new PlainTextConstruct(event.getTitle()));
entry.setContent(new PlainTextConstruct(event.getDescription()));
gbaseAttributes.setItemType("events and activities");
// this sample currently only demonstrates publishing all-day events
// an event in Google Base must have a start-time and end-time, so
// this simulates that by adding 1 day to the end-date specified, if the
// start and end times are identical
DateTime startDateTime = new DateTime(event.getStartDate());
startDateTime.setDateOnly(true);
DateTime endDateTime = null;
if (event.getStartDate().equals(event.getEndDate())) {
Calendar endDateCal = new GregorianCalendar();
endDateCal.setTime(event.getEndDate());
endDateCal.add(Calendar.DATE, 1);
endDateTime = new DateTime(endDateCal.getTime());
} else {
endDateTime = new DateTime(event.getEndDate());
}
endDateTime.setDateOnly(true);
gbaseAttributes.addDateTimeRangeAttribute("event date range",
new DateTimeRange(startDateTime, endDateTime));
gbaseAttributes.addTextAttribute("event performer", "Google mashup test");
gbaseAttributes.addUrlAttribute("performer url", "http://code.google.com/apis/gdata.html");
if (event.getBaseUrl() != null) {
// updating event
baseService.update(event.getBaseUrl(), entry);
} else {
// insert event
GoogleBaseEntry resultEntry = baseService.insert(
FeedURLFactory.getDefault().getItemsFeedURL(),
entry);
updateSsEventEditUrl(event.getSsEditUrl(),
null,
resultEntry.getEditLink().getHref() );
}
}
/**
* Updates the Google Base and/or Calendar edit URLs in the spreadsheet.
* The storage of these edit URLs enables further runs of this code to
* publish events as new Calendar and Base entries only if the events had
* not been previously published
*
* @param ssEditUrl The edit <code>URL</code> for the spreadsheet
* @param calEditUrl The edit URL to be set in the spreadsheet or null
* @param baseEditUrl The edit URL to be set in the spreadsheet or null
* @throws EPAuthenticationException
* @throws IOException
* @throws ServiceException
*/
private void updateSsEventEditUrl(URL ssEditUrl, String calEditUrl,
String baseEditUrl)
throws EPAuthenticationException, IOException, ServiceException {
SpreadsheetService ssService = getSsService();
ListEntry ssEntry = ssService.getEntry(ssEditUrl, ListEntry.class);
// remove spaces in the URL
String calUrlFieldName = fieldMap.getCalendarUrlColumn();
if (calEditUrl == null &&
(ssEntry.getCustomElements().getValue(calUrlFieldName) == null ||
"".equals(ssEntry.getCustomElements().getValue(calUrlFieldName)))){
calEditUrl = " ";
}
if (calEditUrl != null) {
ssEntry.getCustomElements().setValueLocal(
calUrlFieldName,
calEditUrl);
}
// remove spaces in the URL
String baseUrlFieldName = fieldMap.getBaseUrlColumn();
if (baseEditUrl == null &&
(ssEntry.getCustomElements().getValue(baseUrlFieldName) == null ||
"".equals(ssEntry.getCustomElements().getValue(baseUrlFieldName)))){
baseEditUrl = " ";
}
if (baseEditUrl != null) {
ssEntry.getCustomElements().setValueLocal(
baseUrlFieldName,
baseEditUrl);
}
ssEntry.update();
}
/**
* Retrieves a <code>List</code> of <code>SpreadsheetEntry</code> instances
* available from the list of Spreadsheets accessible from the authenticated
* account.
*
* @throws EPAuthenticationException
*/
private List<SpreadsheetEntry> getSsListHelper()
throws EPAuthenticationException {
List<SpreadsheetEntry> returnList = null;
try {
SpreadsheetService ssService = getSsService();
SpreadsheetFeed ssFeed = ssService.getFeed(
new URL(SPREADSHEETS_META_FEED),
SpreadsheetFeed.class);
returnList = ssFeed.getEntries();
} catch (com.google.gdata.util.AuthenticationException authEx) {
throw new EPAuthenticationException(
"SS list read access not available");
} catch (com.google.gdata.util.ServiceException svcex) {
System.err.println("ServiceException while retrieving " +
"available spreadsheets: " + svcex.getMessage());
returnList = null;
} catch (java.io.IOException ioex) {
System.err.println("IOException while retrieving " +
"available spreadsheets: " + ioex.getMessage());
returnList = null;
}
return returnList;
}
/**
* Retrieves a <code>List</code> of <code>WorksheetEntry</code> instances
* available from the list of Worksheets in the specified feed
*
* @param wsFeedUrl The feed of worksheets
* @throws EPAuthenticationException
* @return List of worksheets in the specified feed
*/
private List<WorksheetEntry> getWsListHelper(String wsFeedUrl)
throws EPAuthenticationException {
List<WorksheetEntry> returnList = null;
try {
SpreadsheetService ssService = getSsService();
WorksheetFeed wsFeed = ssService.getFeed(
new URL(wsFeedUrl),
WorksheetFeed.class);
returnList = wsFeed.getEntries();
} catch (com.google.gdata.util.AuthenticationException authEx) {
throw new EPAuthenticationException(
"WS list read access not available");
} catch (com.google.gdata.util.ServiceException svcex) {
System.err.println("ServiceException while retrieving " +
"available worksheets: " + svcex.getMessage());
returnList = null;
} catch (java.io.IOException ioex) {
System.err.println("IOException while retrieving " +
"available worksheets: " + ioex.getMessage());
returnList = null;
}
return returnList;
}
/**
* Retrieves a list of column headers in the specified cell feed
*
* @param cellFeedUrl The cell feed
* @return <code>List</code> of column headers as <code>String</code>s
* @throws EPAuthenticationException
*/
public List<String> getColumnList(String cellFeedUrl)
throws EPAuthenticationException {
List<String> returnList = new LinkedList<String>();
List<CellEntry> columnList = getColumnListHelper(cellFeedUrl);
for (CellEntry columnEntry : columnList) {
returnList.add(columnEntry.getCell().getValue());
}
return returnList;
}
/**
* Retrieves a list of column headers in the specified cell feed
*
* @param cellFeedUrl The cell feed
* @return <code>List</code> of column headers as <code>CellEntry</code>s
* @trhows EPAuthenticationException
*/
private List<CellEntry> getColumnListHelper(String cellFeedUrl)
throws EPAuthenticationException {
List<CellEntry> returnList = null;
try {
SpreadsheetService ssService = getSsService();
CellFeed cellFeed = ssService.getFeed(
new URL(cellFeedUrl + "?min-row=1&max-row=1"),
CellFeed.class);
returnList = cellFeed.getEntries();
} catch (com.google.gdata.util.AuthenticationException authEx) {
throw new
EPAuthenticationException(
"SS read access not available");
} catch (com.google.gdata.util.ServiceException svcex) {
// log general service exception
System.err.println("ServiceException while retrieving " +
"column list: " + svcex.getMessage());
returnList = null;
} catch (java.io.IOException ioex) {
System.err.println("IOException while retrieving " +
"column list: " + ioex.getMessage());
returnList = null;
}
return returnList;
}
/**
* Retrieves all <code>ListEntry</code> objects from the spreadsheet
*
* @return <code>List</code> of <code>ListEntry</code> instances
* @throws EPAuthenticationException
*/
private List<ListEntry> getSsEntryListHelper()
throws EPAuthenticationException {
List<ListEntry> returnList = null;
try {
SpreadsheetService ssService = getSsService();
ListFeed listFeed = ssService.getFeed(ssUrl, ListFeed.class);
returnList = listFeed.getEntries();
} catch (com.google.gdata.util.AuthenticationException authEx) {
throw new EPAuthenticationException("SS read access not available");
} catch (com.google.gdata.util.ServiceException svcex) {
System.err.println("ServiceException while retrieving " +
"entry list: " + svcex.getMessage());
returnList = null;
} catch (java.io.IOException ioex) {
System.err.println("IOException while retrieving " +
"entry list: " + ioex.getMessage());
returnList = null;
}
return returnList;
}
}