/* * To change this template, choose Tools | Templates * and open the template in the editor. */ package CPS.Core.TODOLists; import CPS.Data.CPSComparators; import CPS.Data.CPSComplexPlantingFilter; import CPS.Data.CPSPlanting; import CPS.Module.CPSDataModel; import CPS.Module.CPSGlobalSettings; import CPS.Module.CPSModule; import CPS.ModuleManager; import CPS.UI.Swing.CPSErrorDialog; import CPS.UI.Swing.CPSInfoDialog; import ca.odell.glazedlists.EventList; import ca.odell.glazedlists.FilterList; import ca.odell.glazedlists.GlazedLists; import ca.odell.glazedlists.GroupingList; import com.google.gdata.client.GoogleAuthTokenFactory.UserToken; import com.google.gdata.client.GoogleService.CaptchaRequiredException; import com.google.gdata.client.calendar.CalendarService; import com.google.gdata.data.DateTime; import com.google.gdata.data.Link; import com.google.gdata.data.PlainTextConstruct; import com.google.gdata.data.batch.BatchOperationType; import com.google.gdata.data.batch.BatchStatus; import com.google.gdata.data.batch.BatchUtils; import com.google.gdata.data.calendar.CalendarEntry; import com.google.gdata.data.calendar.CalendarEventEntry; import com.google.gdata.data.calendar.CalendarEventFeed; import com.google.gdata.data.calendar.CalendarFeed; import com.google.gdata.data.calendar.ColorProperty; import com.google.gdata.data.calendar.HiddenProperty; import com.google.gdata.data.calendar.SelectedProperty; import com.google.gdata.data.extensions.ExtendedProperty; import com.google.gdata.data.extensions.When; import com.google.gdata.util.AuthenticationException; import com.google.gdata.util.ServiceException; import java.awt.Component; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.TimeZone; import java.util.prefs.Preferences; import javax.swing.JPanel; /** * * @author crcarter */ public class GoogleCalExporter { // The base URL for a user's calendar metafeed (needs a username appended). private static final String METAFEED_URL_BASE = "https://www.google.com/calendar/feeds/"; // The string to add to the user's metafeedUrl to access the owncalendars // feed. private static final String OWNCALENDARS_FEED_URL_SUFFIX = "/owncalendars/full"; // The string to add to the user's metafeedUrl to access the event feed for // their primary calendar. private static final String EVENT_FEED_URL_SUFFIX = "/private/full"; private static final String CALENDAR_TITLE_PREFIX = "Crop Plan: "; // The HEX representation of red, blue and green private static final String RED = "#A32929"; private static final String BLUE = "#2952A3"; private static final String GREEN = "#0D7813"; /** * Utility classes should not have a public or default constructor. */ protected GoogleCalExporter() {} protected static void exportCropPlan( Component parent, List<CPSPlanting> plantings, String planName, boolean changeLogin ) { //************************************************************************// // Setup the Lists for the crop plan //************************************************************************// EventList<CPSPlanting> planList = GlazedLists.eventList( plantings ); CalendarService service = new CalendarService("CropPlanning-GCal-v0.1"); // load preferences for this class Preferences prefs = Preferences.userNodeForPackage( GoogleCalExporter.class ); //************************************************************************// // log in the user and get the authenticated username //************************************************************************// String userName = authenticateUser( parent, service, prefs.get( "GOOGLE_USERNAME", "" ), changeLogin ? null : prefs.get( "GOOGLE_AUTH_TOKEN", null ) ); if ( userName == null ) { new CPSErrorDialog( parent, "<center>Failed to login to with Google").setVisible( true ); return; } // save the username and auth token for this user prefs.put( "GOOGLE_USERNAME", userName ); prefs.put( "GOOGLE_AUTH_TOKEN", ((UserToken) service.getAuthTokenFactory() .getAuthToken()).getValue()); //************************************************************************// // //************************************************************************// URL calendarsFeedURL; try { calendarsFeedURL = new URL(METAFEED_URL_BASE + userName + OWNCALENDARS_FEED_URL_SUFFIX ); } catch ( MalformedURLException e ) { new CPSErrorDialog( parent, "Error 801", "There is a problem communicating with Google<br>" + "Please verify that your username (email) is entered<br>" + "correctly. If it is, please email us for help." ).setVisible( true ); e.printStackTrace(); return; } // Maps to hold all of our new (and old) feed entries HashMap<String, CalendarEventEntry> planMap = new HashMap<String, CalendarEventEntry>(); HashMap<String, CalendarEventEntry> feedMap = new HashMap<String, CalendarEventEntry>(); //************************************************************************// // now start doing the real work //************************************************************************// try { // look for this crop plan in the calendars list CalendarEntry cal = findCalendarForCropPlan( service, calendarsFeedURL, planName ); // if not found, create calendar if ( cal == null ) { cal = createCalendar( service, calendarsFeedURL, planName ); } // it's been found or created, so find the Calendar ID String[] s = cal.getEditLink().getHref().split( "/" ); String calID = s[ s.length - 1 ].replaceAll( "%40", "@" ); // Create URL for cropplan calendar and URL eventFeedUrl = new URL( METAFEED_URL_BASE + calID + EVENT_FEED_URL_SUFFIX ); //****************************************************************************// // Iterate over all of the seeding days //****************************************************************************// GroupingList<CPSPlanting> groupList = new GroupingList<CPSPlanting>( planList, new CPSComparators.DatePlantComparator() ); for ( Iterator<List<CPSPlanting>> it = groupList.iterator(); it.hasNext(); ) { // get the list of plantings that happen on the next date List<CPSPlanting> dateGroup = it.next(); // start the entries for this date StringBuilder dsTitle = new StringBuilder( "Seed in Field: " ); StringBuilder tpTitle = new StringBuilder( "Seed in GH: " ); String dsDelim = ""; String tpDelim = ""; String dsContent = ""; String tpContent = ""; // they're all on the same date Date d = dateGroup.get(0).getDateToPlant(); // now group these plantings by crop name ... GroupingList<CPSPlanting> cropGroupList = new GroupingList<CPSPlanting>( GlazedLists.eventList( dateGroup ), new CPSComparators.CropNameComparator() ); // ... and iterate over the grouped crops for ( Iterator<List<CPSPlanting>> it2 = cropGroupList.iterator(); it2. hasNext(); ) { List<CPSPlanting> cropGroup = it2.next(); // we assume that each group will be all DS or all TP if ( cropGroup.get(0).isDirectSeeded() ) { // append this crop name to the list of crops happening today dsTitle.append( dsDelim ).append( cropGroup.get(0).getCropName() ); dsDelim = ", "; // and for each planting, add some info about it to the // description for this event for ( CPSPlanting p : cropGroup ) { dsContent += p.getCropName() + ": " + p.getVarietyName() + " - " + p.getBedsToPlantString() + " beds\n"; } } else { // ... ditto tpTitle.append( tpDelim ).append(cropGroup.get(0).getCropName()); tpDelim = ", "; for ( CPSPlanting p : cropGroup ) { tpContent += p.getCropName() + ": " + p.getVarietyName() + " - " + p.getFlatsNeededString() + " x " + p.getFlatSize() + "\n"; } } } // if there are DS plantings (we check to see if there is any content // created for this event) then we create an entry and add it to our map if ( ! dsContent.equals( "" ) ) { CalendarEventEntry dsee = createEvent( dsTitle.toString(), dsContent, d ); planMap.put( "ds"+d.getTime(), dsee ); } // ... ditto if ( ! tpContent.equals( "" ) ) { CalendarEventEntry tpee = createEvent( tpTitle.toString(), tpContent, d ); planMap.put( "gh"+d.getTime(), tpee); } } //****************************************************************************// // Now do the same but for transplanted plantings and transplant dates //****************************************************************************// // whittle it down to just TP plantings, then group by TP date FilterList<CPSPlanting> fl = new FilterList<CPSPlanting>( planList ); fl.setMatcher( CPSComplexPlantingFilter.transplantedFilter() ); groupList = new GroupingList<CPSPlanting>( fl, new CPSComparators.DateTPComparator() ); // iterate over TP dates for ( Iterator<List<CPSPlanting>> it = groupList.iterator(); it.hasNext(); ) { List<CPSPlanting> dateGroup = it.next(); StringBuilder tpTitle = new StringBuilder( "TP in Field: " ); String tpDelim = ""; String tpContent = ""; Date d = dateGroup.get(0).getDateToTP(); GroupingList<CPSPlanting> cropGroupList = new GroupingList<CPSPlanting>( GlazedLists.eventList( dateGroup ), new CPSComparators.CropNameComparator() ); // iterate over the grouped crops for ( Iterator<List<CPSPlanting>> it2 = cropGroupList.iterator(); it2. hasNext(); ) { List<CPSPlanting> cropGroup = it2.next(); // we assume that each group will be all DS or all TP String c = ""; // now iterate over the actual plantings for ( CPSPlanting p : cropGroup ) { c += p.getCropName() + ": " + p.getVarietyName() + " - " + p.getBedsToPlantString() + " beds\n"; } tpTitle.append( tpDelim ).append(cropGroup.get(0).getCropName()); tpDelim = ", "; tpContent += c; } CalendarEventEntry tpee = createEvent( tpTitle.toString(), tpContent, d ); planMap.put( "tp"+d.getTime(), tpee ); } //****************************************************************************// // done loading all local event, now load all remote events (if any) //****************************************************************************// CalendarEventFeed planCalFeed; Link batchLink = null; Link nextLink = new Link(); nextLink.setHref( eventFeedUrl.toString() ); // build a map for the entries which have already been uploaded // the feed is paginated, so we have to iterate w/ the getNextLink() while( nextLink != null ) { planCalFeed = service.getFeed( new URL( nextLink.getHref() ), CalendarEventFeed.class ); if ( batchLink == null ) batchLink = planCalFeed.getLink( Link.Rel.FEED_BATCH, Link.Type.ATOM ); for ( CalendarEventEntry entry : planCalFeed.getEntries()) { List<ExtendedProperty> props = entry.getExtendedProperty(); ExtendedProperty eventID = null; for ( ExtendedProperty extendedProperty : props ) { if ( extendedProperty.getName().equals( "cpsEventID" ) ) { eventID = extendedProperty; break; } } if ( eventID != null ) feedMap.put( eventID.getValue(), entry ); } nextLink = planCalFeed.getNextLink(); } //****************************************************************************// // Local and remote entries created, now match them up and batch upload //****************************************************************************// CalendarEventFeed batchRequest = new CalendarEventFeed(); int updates, inserts, deletes; updates = inserts = deletes = 0; // iterate over all local keys looking for matching remote keys for ( String key : planMap.keySet() ) { CalendarEventEntry cee = planMap.get( key ); // UPDATE existing entries if ( feedMap.containsKey( key ) ) { updates++; CalendarEventEntry fee = feedMap.get( key ); fee.setTitle( cee.getTitle() ); fee.setContent( cee.getContent() ); BatchUtils.setBatchId( fee, key ); BatchUtils.setBatchOperationType( fee, BatchOperationType.UPDATE ); batchRequest.getEntries().add( fee ); feedMap.remove( key ); } else { // INSERT new entries inserts++; ExtendedProperty ep = new ExtendedProperty(); ep.setName( "cpsEventID" ); ep.setValue( key ); cee.addExtendedProperty( ep ); BatchUtils.setBatchId( cee, key ); BatchUtils.setBatchOperationType( cee, BatchOperationType.INSERT ); batchRequest.getEntries().add( cee ); } } // DELETE entries no longer relevant for ( String key : feedMap.keySet() ) { deletes++; CalendarEventEntry fee = feedMap.get( key ); BatchUtils.setBatchId( fee, key ); BatchUtils.setBatchOperationType( fee, BatchOperationType.DELETE ); batchRequest.getEntries().add( fee ); } //****************************************************************************// // do the batch POST //****************************************************************************// CalendarEventFeed batchResponse = service.batch( new URL(batchLink.getHref()), batchRequest ); //****************************************************************************// // Ensure that all the operations were successful. //****************************************************************************// boolean isSuccess = true; for (CalendarEventEntry entry : batchResponse.getEntries()) { String batchId = BatchUtils.getBatchId(entry); if (!BatchUtils.isSuccess(entry)) { isSuccess = false; BatchStatus status = BatchUtils.getBatchStatus(entry); CPSModule.debug("GCal", "\n" + batchId + " failed (" + status.getReason() + ") " + status.getContent()); } } if (isSuccess) { new CPSInfoDialog( parent, "Success!", "<center>Your plan has been synced<br>with Google Calendar.<br>" + "Created: " + inserts + "<br>" + "Updated: " + updates + "<br>" + "Deleted: " + deletes + "" ).setVisible( true ); } } catch (MalformedURLException e) { // Bad URL new CPSErrorDialog( parent, "Error 803", "There is a problem communicating with Google<br>" + "Please email us for help." ).setVisible( true ); e.printStackTrace(); } catch (IOException e) { // Communications error new CPSErrorDialog( parent, "Error 804", "There is a problem communicating with Google<br>" + "Please email us for help." ).setVisible( true ); e.printStackTrace(); } catch (ServiceException e) { // Server side error new CPSErrorDialog( parent, "Error 805", "There is a problem communicating with Google<br>" + "Please email us for help." ).setVisible( true ); e.printStackTrace(); } } private static String authenticateUser( Component parent, CalendarService service, String userName, String authToken ) { // attempt to authenticate w/ stored auth token CPSModule.debug("GCal", "Logging in with auth token for account " + userName ); service.setUserToken( authToken ); URL calendarsFeedURL; try { calendarsFeedURL = new URL(METAFEED_URL_BASE + userName + OWNCALENDARS_FEED_URL_SUFFIX ); } catch ( MalformedURLException e ) { e.printStackTrace(); return null; } while ( ! isAuthenticated( service, calendarsFeedURL ) ) { CPSModule.debug("GCal", "Logging in with user credentials" ); GoogleCalLoginDialog loginDia = new GoogleCalLoginDialog( parent, userName ); loginDia.setVisible( true ); // bail if cancelled if ( loginDia.isCancelled() ) { return null; } // if they enter a different username, then rebuild the feed URL if ( ! userName.equalsIgnoreCase( loginDia.getEmail() ) ) { userName = loginDia.getEmail(); try { calendarsFeedURL = new URL(METAFEED_URL_BASE + userName + OWNCALENDARS_FEED_URL_SUFFIX ); } catch ( MalformedURLException e ) { e.printStackTrace(); } } // login w/ user name and password char[] p = loginDia.getPassword(); try { service.setUserCredentials( userName, new String( p ) ); } catch ( CaptchaRequiredException captchaException ) { // we have to handle a captcha CPSModule.debug("GCal", "Captcha error: " + captchaException.getMessage() ); GoogleCaptchaDialog captchaDialog = new GoogleCaptchaDialog(); try { captchaDialog.setCaptchaUrl( new URL( captchaException.getCaptchaUrl() )); captchaDialog.setVisible( true ); } catch ( MalformedURLException f ) { CPSModule.debug("GCal", "WTF?! Why did Google give us a bad CAPTCHA URL?" ); CPSModule.debug("GCal", f.getMessage() ); } if ( ! captchaDialog.getCaptchaAnswer().equals( "" ) ) { try { service.setUserCredentials( userName, new String( p ), captchaException.getCaptchaToken(), captchaDialog.getCaptchaAnswer() ); } catch ( AuthenticationException g ) { CPSErrorDialog ed = new CPSErrorDialog( parent, "Invalid Credentials", "<center>Google said that you entered invalid credentials.<br>" + "This could mean that the email address and/or the<br>" + "password you entered were incorrect. Please try again." ); ed.setVisible( true ); } } } catch (AuthenticationException e ) { CPSErrorDialog ed = new CPSErrorDialog( parent, "Invalid Credentials", "<center>Google said that you entered invalid credentials.<br>" + "This could mean that the email address and/or the<br>" + "password you entered were incorrect. Please try again." ); ed.setVisible( true ); } // clear out the password array for safety // I believe that this also clears out the password that was saved in // the dialog ... which is what we want Arrays.fill( p, '0' ); } return userName; } /** * Creates a new secondary calendar using the owncalendars feed. * * @param service An authenticated CalendarService object. * @param cropPlan Name of crop plan this calendar will represent * @return The newly created calendar entry. * @throws IOException If there is a problem communicating with the server. * @throws ServiceException If the service is unable to handle the request. */ private static CalendarEntry createCalendar( CalendarService service, URL calendarFeed, String cropPlan ) throws IOException, ServiceException { CPSModule.debug( "GCal", "Creating calendar: " + CALENDAR_TITLE_PREFIX + cropPlan ); // Create the calendar CalendarEntry calendar = new CalendarEntry(); calendar.setTitle(new PlainTextConstruct( CALENDAR_TITLE_PREFIX + cropPlan )); calendar.setSummary(new PlainTextConstruct( "Contains events representing crop plan: " + cropPlan )); calendar.setHidden( HiddenProperty.FALSE ); calendar.setSelected( SelectedProperty.TRUE ); calendar.setColor( new ColorProperty( GREEN ) ); // Insert the calendar return service.insert(calendarFeed, calendar); } private static CalendarEntry findCalendarForCropPlan( CalendarService service, URL calendarFeed, String cropPlan ) throws IOException, ServiceException { CalendarFeed resultFeed = service.getFeed( calendarFeed, CalendarFeed.class); for ( CalendarEntry entry : resultFeed.getEntries() ) { if ( entry.getTitle().getPlainText().equals( CALENDAR_TITLE_PREFIX + cropPlan ) ) return entry; } // calendar not found return null; } /** * Helper method to create either single-instance or recurring events. For * simplicity, some values that might normally be passed as parameters (such * as author name, email, etc.) are hard-coded. * * @param service An authenticated CalendarService object. * @param eventTitle Title of the event to create. * @param eventContent Text content of the event to create. * @param recurData Recurrence value for the event, or null for * single-instance events. * @param isQuickAdd True if eventContent should be interpreted as the text of * a quick add event. * @param wc A WebContent object, or null if this is not a web content event. * @return The newly-created CalendarEventEntry. * @throws ServiceException If the service is unable to handle the request. * @throws IOException Error communicating with the server. */ private static CalendarEventEntry createEvent( String eventTitle, String eventContent, Date eventDate ) throws ServiceException, IOException { CalendarEventEntry myEntry = new CalendarEventEntry(); myEntry.setTitle(new PlainTextConstruct(eventTitle)); myEntry.setContent(new PlainTextConstruct(eventContent)); myEntry.setQuickAdd(false); // If a recurrence was requested, add it. Otherwise, set the // time (the current date and time) and duration (30 minutes) // of the event. DateTime eDate = new DateTime( eventDate, TimeZone.getDefault() ); eDate.setDateOnly( true ); When eventTimes = new When(); eventTimes.setStartTime(eDate); eventTimes.setEndTime(eDate); myEntry.addTime(eventTimes); return myEntry; // Send the request and receive the response: // return service.insert(eventFeedUrl, myEntry); } private static boolean isAuthenticated( CalendarService service, URL feedURL ) { try { service.getFeed( feedURL, CalendarFeed.class ); return true; } catch ( Exception e ) { CPSModule.debug("GCal", "Authentication failed with message: " + e.getMessage() ); return false; } } public static void main(String[] args) { System.out.println( "Opening DM..." ); CPSGlobalSettings.setDebug( false ); CPSGlobalSettings.setTempOutputDir( "/Users/crcarter/Documents/Crop Plans/cps-devel" ); ModuleManager mm = new ModuleManager(); CPSDataModel dm = mm.getDM(); System.out.println( "Initing DM..." ); dm.init(); String planName = dm.getListOfCropPlans().get(0); System.out.println( "Getting crop plan: " + planName ); exportCropPlan( new JPanel(), dm.getCropPlan( planName ), planName, false ); System.exit( 0 ); } }