GData API Development

I’m working on a small app to sync a local calendar database with a Google Calendar using the GData API for Java.  I’ll be keeping everyone up to date on this page.

Update:  The code for GoogleSync DR1:

import gnu.getopt.Getopt;
import gnu.getopt.LongOpt;

import java.io.IOException;
import java.net.URL;
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Date;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.List;

import com.google.gdata.client.calendar.CalendarService;
import com.google.gdata.data.DateTime;
import com.google.gdata.data.HtmlTextConstruct;
import com.google.gdata.data.PlainTextConstruct;
import com.google.gdata.data.calendar.CalendarEventEntry;
import com.google.gdata.data.extensions.When;
import com.google.gdata.util.AuthenticationException;
import com.google.gdata.util.ServiceException;

/*
 * TODO add regex validation for Google Calendar username as valid e-mail address.
 * TODO add help text.
 * TODO add version message.
 */

/*
 * This is very GNU-centric code.  It's portable but will still behave as if it were a GNU command.
 */

/**
 * @author imbrius An executable class that connects to a Drupal database,
 *         extracts events that are flagged to by synched with Google Calendar,
 *         connects to Google Calendar, and uploads the marked Drupal events.
 */
public class GoogleSync {

	/**
	 * The string to send to Google Calendar as the organization's name. Cannot
	 * have spaces or dashes.
	 */
	public static final String ORGNAME = "lakemasoniccenter";

	/**
	 * The string to send to Google Calendar as the name of the program. Cannot
	 * have spaces or dashes.
	 */
	public static final String PROGNAME = "googlesync";

	/**
	 * The string to send to Google Calendar as the version of the program.
	 * Cannot have spaces or dashes.
	 */
	public static final String VERSION = "0.1";

	/**
	 * The database (MySQL) server to connect to if none is specified.
	 */
	public static final String DEFAULT_DBHOST = "localhost";

	/**
	 * The database schema to use if none is specified.
	 */
	public static final String DEFAULT_DBSCHEMA = "drupaldb";

	/**
	 * The MySQL server port to use if none is specified.
	 */
	public static final int DEFAULT_DBPORT = 3306;

	/**
	 * @param args
	 *            the command line arguments
	 * @throws ImpossibleReturnException
	 *             when a call to getopt() returns 0.
	 * @throws UnhandledCaseException
	 *             when getopt() returns a character that doesn't match any
	 *             known option case.
	 * @throws SQLException
	 *             when an SQL query generates an error on the MySQL server.
	 * @throws ClassNotFoundException
	 *             when the MySQL Connector/J driver cannot be loaded.
	 * @throws IOException
	 *             when the app fails to connect to Google due to network
	 *             errors.
	 */
	public static void main(String[] args) throws ImpossibleReturnException,
			UnhandledCaseException, ClassNotFoundException, SQLException,
			IOException {
		String dbUser = null; // no reasonable default
		String dbPass = ""; // allows for empty password
		String dbHost = DEFAULT_DBHOST;
		int dbPort = DEFAULT_DBPORT;
		String dbSchema = DEFAULT_DBSCHEMA;

		// this sets our default last modified date to be yesterday
		Calendar gregorianCalendar = GregorianCalendar.getInstance();
		gregorianCalendar.add(Calendar.DAY_OF_MONTH, -1);
		Date lastChanged = new java.sql.Date(gregorianCalendar
				.getTimeInMillis());

		// command line argument parsing
		try {
			int c; // the character representing the option the user chose
			LongOpt[] longopts = new LongOpt[8];
			//
			// StringBuffer sb = new StringBuffer(); // note - not StringBugger
			// like
			// I keep wanting to type!
			longopts[0] = new LongOpt("help", LongOpt.NO_ARGUMENT, null, 'h');
			longopts[1] = new LongOpt("version", LongOpt.NO_ARGUMENT, null, 'v');
			longopts[2] = new LongOpt("dbuser", LongOpt.REQUIRED_ARGUMENT,
					null, 'u');
			longopts[3] = new LongOpt("dbpass", LongOpt.REQUIRED_ARGUMENT,
					null, 'p');
			longopts[4] = new LongOpt("dbhost", LongOpt.REQUIRED_ARGUMENT,
					null, 'H');
			longopts[5] = new LongOpt("dbport", LongOpt.REQUIRED_ARGUMENT,
					null, 'P');
			longopts[6] = new LongOpt("dbschema", LongOpt.REQUIRED_ARGUMENT,
					null, 's');
			longopts[7] = new LongOpt("lastchanged", LongOpt.REQUIRED_ARGUMENT,
					null, 'e');
			//
			Getopt g = new Getopt(PROGNAME, args, "-:hvu:p:H:P:s:e:", longopts);
			g.setOpterr(true); // complain to the user when a REQUIRED_ARGUMENT
			// isn't present
			//
			while ((c = g.getopt()) != -1) {
				switch (c) {
				case 0:
					// can't happen
					throw new ImpossibleReturnException(
							"A call to getopt() returned 0.  This should not happen!");

				case 1:
					// someone tried to give us data without an option - doesn't
					// work that way
					throw new ArgumentException(
							"Argument given without option.");

				case 'h':
					printHelpMessage();
					System.exit(0);
					break; // unreachable code

				case 'v':
					printVersionInfo();
					System.exit(0);
					break; // unreachable code

				case 'u':
					dbUser = g.getOptarg();
					break;

				case 'p':
					dbPass = g.getOptarg();
					break;

				case 'H':
					dbHost = g.getOptarg();
					break;

				case 'P':
					dbPort = Integer.parseInt(g.getOptarg());
					break;

				case 's':
					dbSchema = g.getOptarg();
					break;

				case 'e':
					lastChanged = (Date) new SimpleDateFormat().parse(g
							.getOptarg());
					break;

				case '?': // Getopt didn't find the option in the list of
					// choices we gave it
					throw new ArgumentException("The option "
							+ (char) g.getOptopt() + " is not valid.");

				default:
					// shouldn't happen
					throw new UnhandledCaseException(
							"The programmer screwed up the command-line argument handling.  This is a bug.");

				}
			}

			for (int i = g.getOptind(); i < args.length; i++) {
				// This shouldn't be able to happen - getopt() returns 1 when
				// there's an option argument given with no option specified.
				// But if the user terminates the options list with a -- and
				// then puts stuff, getopt() will return -1 and break the loop
				// and this code will handle the arguments after the --.
				System.out
						.println("I don't know what "
								+ args[i]
								+ " means so I'm ignoring it.  I hope that's okay.  If not, CTRL+C is your friend right now.");
			}

		}

		catch (ArgumentException argex) {
			System.err.println(argex.getMessage());
			System.exit(-1);
		}

		catch (ParseException pex) {
			System.err.println(pex.getMessage()
					+ " Unable to parse the date format given.");
			System.exit(-1);
		}

		try {
			if (dbUser == null || dbPass == null || dbUser.isEmpty()) {
				// I had to fix this to put the null checks first in case dbUser
				// really is null. Calling isEmpty() would cause a
				// NullPointerException.
				System.err
						.println("Username and password need to be specified.");
				System.exit(-1);
			}

			List<Event> eventList = getMarkedEvents(dbUser, dbPass, dbHost,
					dbPort, dbSchema, lastChanged);
			List<ConnectionBucket> connectionBuckets = makeConnectionBuckets(eventList);

			for (ConnectionBucket cb : connectionBuckets) {
				CalendarService calService = new CalendarService(
						makeGoogleAppString());
				calService.setUserCredentials(cb.getUsername(), cb
						.getPassword());

				// This might be an old URL. If it fails with an HTTP exception,
				// try changing this.
				// TODO look up how to post events to a calendar other than the
				// user's private calendar.
				URL postUrl = new URL("http://www.google.com/calendar/feeds/"
						+ dbUser + "/private/full");

				for (Event e : cb) {
					try {
						CalendarEventEntry entry = new CalendarEventEntry();

						entry.setTitle(new PlainTextConstruct(e.getTitle()));
						entry.setContent(new HtmlTextConstruct(e.getBody()));

						DateTime startTime = DateTime.parseDateTime(e
								.getEventStart().toString());
						DateTime endTime = DateTime.parseDateTime(e
								.getEventEnd().toString());
						When eventTimes = new When();
						eventTimes.setStartTime(startTime);
						eventTimes.setEndTime(endTime);
						entry.addTime(eventTimes);

						calService.insert(postUrl, entry);

						System.out.println("Inserted entry " + e.toString());

					}

					catch (ServiceException sex) {
						System.err.println("Insert failed for event "
								+ e.toString() + ".  Google said: "
								+ sex.getMessage());
					}

				}

			}

		}

		catch (IllegalPortException ipex) {
			System.err.println(ipex.getMessage());
			System.exit(-1);
		}

		catch (AuthenticationException authex) {
			System.err.println(authex.getMessage());
			System.exit(-1);
		}

	}

	/**
	 * Combines the organization name, program name, and version to send to
	 * Google.
	 * 
	 * @return the formatted Google App string
	 */
	private static final String makeGoogleAppString() {
		return new String(ORGNAME + "-" + PROGNAME + "-" + VERSION);
	}

	/**
	 * Prints the version info to the command line.
	 * 
	 */
	private static void printVersionInfo() {
		// TODO Auto-generated method stub

	}

	/**
	 * Prints the GNU help message to the command line.
	 * 
	 */
	private static void printHelpMessage() {
		// TODO Auto-generated method stub

	}

	/**
	 * Checks for similarities in event metadata and bunches them together to
	 * minimize the number of connections needed to Google.
	 * 
	 * @param eventList
	 *            the list of events to check
	 * @return a list of ConnectionBuckets, each with similar events in them
	 */
	private static List<ConnectionBucket> makeConnectionBuckets(
			List<Event> eventList) {
		List<ConnectionBucket> connectionBuckets = new ArrayList<ConnectionBucket>();

		for (Event e : eventList) {
			boolean bucketFound = false;

			for (ConnectionBucket cb : connectionBuckets) {
				if (e.getCalendarConnection().equals(cb.toString())) {
					// We found a bucket that matches this event, so add it.
					cb.add(e);
					bucketFound = true;
				}
			}

			if (!bucketFound) {
				// We did not find a bucket to match the event, so make one and
				// then add the event to it.
				ConnectionBucket cb = new ConnectionBucket();
				cb.setCalendarName(e.getGoogleCalendarName());
				cb.setPassword(e.getGoogleCalendarPassword());
				cb.setUsername(e.getGoogleCalendarUsername());
				cb.add(e);
				connectionBuckets.add(cb);
			}
		}

		return connectionBuckets;
	}

	/**
	 * Fetches the marked events from the database.
	 * 
	 * @param dbUser
	 *            the database username
	 * @param dbPass
	 *            the database password
	 * @param dbHost
	 *            the database server hostname
	 * @param dbPort
	 *            the MySQL server port
	 * @param dbSchema
	 *            the schema to use
	 * @param lastChanged
	 *            the minimum last changed date to fetch
	 * @return a list of events marked for Google Calendar sync
	 * @throws ClassNotFoundException
	 *             when the JRE can't load the MySQL Connector/J driver.
	 * @throws SQLException
	 *             when an SQL statement causes an error on the MySQL server.
	 * @throws IllegalPortException
	 *             when the specified port number is less than 1 or greater than
	 *             65535.
	 */
	private static List<Event> getMarkedEvents(String dbUser, String dbPass,
			String dbHost, int dbPort, String dbSchema, Date lastChanged)
			throws ClassNotFoundException, SQLException, IllegalPortException {
		Class.forName("com.mysql.jdbc.Driver");  // MySQL Connector/J driver

		// this code is redundant
//		if (dbHost.isEmpty() || dbHost == null) {
//			dbHost = "localhost"; 
//		}

		if (dbPort < 1 || dbPort > 65535) {
			throw new IllegalPortException(
					"Port number must be between 1 and 65535.");
		}

		Connection con = DriverManager.getConnection("jdbc:mysql://" + dbHost
				+ ":" + dbPort + "/" + dbSchema, dbUser, dbPass);

		CallableStatement cs = con
				.prepareCall("{call lake_get_google_events(?)}");
		ResultSet rs;
		List<Event> markedEvents = new ArrayList<Event>();
		cs.setDate(0, lastChanged);
		rs = cs.executeQuery();

		while (rs.next()) {
			Event evt = new Event();
			evt.setBody(rs.getString("body"));
			evt.setEventEnd(rs.getDate("event_end"));
			evt.setEventStart(rs.getDate("event_start"));
			evt.setGoogleCalendarName(rs
					.getString("field_google_calendar_name_value"));
			evt.setGoogleCalendarPassword(rs
					.getString("field_google_calendar_password_value"));
			evt.setGoogleCalendarUsername(rs
					.getString("field_google_calendar_username_value"));
			evt.setNid(rs.getInt("nid"));
			evt.setTeaser(rs.getString("teaser"));
			evt.setTimezone(rs.getString("timezone"));
			evt.setTitle(rs.getString("title"));
			evt.setCreated(rs.getDate("created"));
			evt.setChanged(rs.getDate("changed"));
			markedEvents.add(evt);
		}

		con.close();
		return markedEvents;
	}

}

Update:  Proof of Concept:  It’s on like Diddy Kong!

Eclipse IDE showing output from GoogleDocs API proof of concept code

Eclipse IDE showing output from GoogleDocs API proof of concept code

I created a document in GoogleDocs and saved it as “It’s on like diddy kong.”  When I run the test code that should display the titles of all my docs, it does indeed show my diddy kong document.  GData API is a go!

Update:  The last two Sequence Diagrams:

Sequence Diagram for List Mode

Sequence Diagram for List Mode

Sequence Diagram for Specifying Criteria

Sequence Diagram for Specifying Criteria

Specifying Criteria is assumed in list mode – if list mode is invoked without criteria-specifying options, then it does the listing using the Default Case below.

Update:  Sequence Diagram for Default Case:

Sequence Diagram for Fetching and Uploading All Drupal Feeds to Google Calendar

Sequence Diagram for Fetching and Uploading All Drupal Feeds to Google Calendar

Update:  New class diagram:

Class Diagram as of 3 PM, Apr. 18

Class Diagram as of 3 PM, Apr. 18

Here’s the class diagram so far:

Class Diagram for a one-way push of events from drupal to Google Calendar

Class Diagram for a one-way push of events from drupal to Google Calendar

Here’s the code I’ve written so far:

/**
 *
 */
package authentication;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.mail.internet.ParseException;

/**
 * @author imbrius
 *
 */
public class UserCredentials {
	public static final String EMAIL_REGEX = ".+@.+\\.[a-z]+";
	public static Pattern pattern;
	private String userName = "";
	private String password = "";

	static {
		pattern = Pattern.compile(EMAIL_REGEX);
	}

	/**
	 * Default constructor
	 */
	public UserCredentials() {
	}

	/**
	 *
	 * @param userName
	 *            the userName to set
	 * @param password
	 *            the password to set
	 * @throws ParseException
	 *             if userName fails validation as e-mail address.
	 */
	public UserCredentials(String userName, String password)
	                    throws ParseException {
		setUserName(userName);
		setPassword(password);
	}

	/**
	 * @param userName
	 *            the userName to set
	 * @throws ParseException
	 *             if userName fails validation as e-mail address.
	 */
	public void setUserName(String userName) throws ParseException {
		if (validateUserName(userName)) {
			this.userName = userName;
		}

		else {
			throw new ParseException(
					"Invalid E-mail address format.  Username must be a valid E-mail address.");
		}
	}

	/**
	 * @return the userName
	 */
	public String getUserName() {
		return userName;
	}

	/**
	 * @param password
	 *            the password to set
	 */
	public void setPassword(String password) {
		this.password = password;
	}

	/**
	 * @return the password
	 */
	public String getPassword() {
		return password;
	}

	private static boolean validateUserName(String uName) {
		Matcher matcher = pattern.matcher(uName);

		boolean found = false;
		while (matcher.find()) {
			found = true;
		}

		return found;
	}

	/**
	 * @return the userName
	 */
	public String toString() {
		return getUserName();
	}

}

And the JUnit test code for this class:

/**
package authentication;

import javax.mail.internet.ParseException;

import junit.framework.TestCase;

/**
 * @author imbrius
 *
 */
public class UserCredentialsTest extends TestCase {
	public static final String TEST_USERNAME = "test@fake.net";
	public static final String TEST_PASSWORD = "password";

	/**
	 * @param name
	 */
	public UserCredentialsTest(String name) {
		super(name);
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see junit.framework.TestCase#setUp()
	 */
	protected void setUp() throws Exception {
		super.setUp();
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see junit.framework.TestCase#tearDown()
	 */
	protected void tearDown() throws Exception {
		super.tearDown();
	}

	/**
	 * Test method for {@link authentication.UserCredentials#UserCredentials()}.
	 */
	public final void testUserCredentials() {
		new UserCredentials();
	}

	/**
	 * Test method for
	 * {@link authentication.UserCredentials#UserCredentials(java.lang.String, java.lang.String)}
	 * .
	 */
	public final void testUserCredentialsStringString() {
		try {
			new UserCredentials(TEST_USERNAME, TEST_PASSWORD);
		} catch (ParseException e) {
			// Auto-generated catch block
			e.printStackTrace();
		}

	}

	/**
	 * Test method for
	 * {@link authentication.UserCredentials#setUserName(java.lang.String)}.
	 */
	public final void testSetUserName() {
		UserCredentials uc = new UserCredentials();

		try {
			uc.setUserName(TEST_USERNAME);
		} catch (ParseException e) {
			// Auto-generated catch block
			e.printStackTrace();
		}

		if (!uc.getUserName().equals(TEST_USERNAME)) {
			fail("Incorrect username.");
		}

	}

	/**
	 * Test method for {@link authentication.UserCredentials#getUserName()}.
	 */
	public final void testGetUserName() {
		try {
			UserCredentials uc = new UserCredentials(TEST_USERNAME,
					TEST_PASSWORD);

			if (!uc.getUserName().equals(TEST_USERNAME)) {
				fail("Incorrect username.");
			}
		} catch (ParseException e) {
			// Auto-generated catch block
			e.printStackTrace();
		}

	}

	/**
	 * Test method for
	 * {@link authentication.UserCredentials#setPassword(java.lang.String)}.
	 */
	public final void testSetPassword() {
		UserCredentials uc = new UserCredentials();

		uc.setPassword(TEST_PASSWORD);

		if (!uc.getPassword().equals(TEST_PASSWORD)) {
			fail("Incorrect password.");
		}

	}

	/**
	 * Test method for {@link authentication.UserCredentials#getPassword()}.
	 */
	public final void testGetPassword() {
		try {
			UserCredentials uc = new UserCredentials(TEST_USERNAME,
					TEST_PASSWORD);

			if (!uc.getPassword().equals(TEST_PASSWORD)) {
				fail("Incorrect password.");
			}

		} catch (ParseException e) {
			// Auto-generated catch block
			e.printStackTrace();
		}

	}

	/**
	 * Test method for {@link authentication.UserCredentials#toString()}.
	 */
	public final void testToString() {
		try {
			UserCredentials uc = new UserCredentials(TEST_USERNAME,
					TEST_PASSWORD);

			if (!uc.toString().equals(TEST_USERNAME)) {
				fail("Incorrect username.");
			}

		} catch (ParseException e) {
			// Auto-generated catch block
			e.printStackTrace();
		}

	}

	/**
	 * Test method for {@link java.lang.Object#hashCode()}.
	 */
	public final void testHashCode() {
		int hashSample1 = 0;
		int hashSample2 = 0;
		int hashSample3 = 0;

		try {
			UserCredentials uc1 = new UserCredentials(TEST_USERNAME,
					TEST_PASSWORD);
			UserCredentials uc2 = new UserCredentials();

			hashSample1 = uc1.hashCode();
			hashSample2 = uc2.hashCode();

			if (hashSample1 == hashSample2) {
				fail("Hash code not unique to object instance.");
			}

			hashSample3 = uc1.hashCode();

			if (hashSample3 != hashSample1) {
				fail("Hash code changed for same object instance.");
			}

		} catch (ParseException e) {
			// Auto-generated catch block
			e.printStackTrace();
		}

	}

	/**
	 * Test method for {@link java.lang.Object#equals(java.lang.Object)}.
	 */
	public final void testEquals() {
		try {
			UserCredentials uc1 = new UserCredentials(TEST_USERNAME,
					TEST_PASSWORD);
			UserCredentials uc2 = new UserCredentials("test2@fake.net",
					"password2");

			if (uc1.equals(uc2)) {
				fail("Different values tested equal.");
			}

		} catch (ParseException e) {
			// Auto-generated catch block
			e.printStackTrace();
			fail(e.getMessage());
		}

	}

}
About these ads

, , , , , , , , , , , , , , , , , , , , , , , ,

  1. #1 by Joshua on April 13, 2010 - 6:56 AM

    I should probably do proper encapsulation for the Pattern so it can’t be modified outside of the class.

  2. #2 by Joshua on May 7, 2010 - 1:15 PM

    Gah! Posting code in wordpress is worse than coding in EMACS on an 80×20 terminal. :P

  1. Code Criticism « Around Teh Table

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 48 other followers

%d bloggers like this: