org.gcaldaemon.core.Synchronizer.java Source code

Java tutorial

Introduction

Here is the source code for org.gcaldaemon.core.Synchronizer.java

Source

//
// GCALDaemon is an OS-independent Java program that offers two-way
// synchronization between Google Calendar and various iCalalendar (RFC 2445)
// compatible calendar applications (Sunbird, Rainlendar, iCal, Lightning, etc).
//
// Apache License
// Version 2.0, January 2004
// http://www.apache.org/licenses/
// 
// Project home:
// http://gcaldaemon.sourceforge.net
//
package org.gcaldaemon.core;

import java.awt.GraphicsEnvironment;
import java.awt.Toolkit;
import java.io.File;
import java.io.FileOutputStream;
import java.io.RandomAccessFile;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.StringTokenizer;

import net.fortuna.ical4j.model.Calendar;
import net.fortuna.ical4j.model.component.VEvent;
import net.fortuna.ical4j.model.component.VTimeZone;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.gcaldaemon.logger.QuickWriter;

import com.google.api.services.calendar.model.Event;

/**
 * Main synchronizer thread.
 * 
 * Created: Jan 03, 2007 12:50:56 PM
 * 
 * @author Andras Berkes
 */
final class Synchronizer extends Thread {

    // --- LOGGER ---

    private static final Log log = LogFactory.getLog(Synchronizer.class);

    // --- ON DEMAND UID REGISTRY ---

    private final HashMap eventRegistry = new HashMap();

    // --- CALENDAR MODIFICATION QUEUE ---

    private final LinkedList changedCalendars = new LinkedList();

    // --- CONSTRUCTOR ---

    private final ProgressMonitor monitor;
    private final File eventRegistryFile;
    private final boolean deleteEnabled;

    Synchronizer(ThreadGroup mainGroup, Configurator configurator) throws Exception {
        super(mainGroup, "Synchronizer");
        setDaemon(true);

        // Show animated progress bar while synching
        if (configurator.getConfigProperty(Configurator.PROGRESS_ENABLED, false)) {
            if (GraphicsEnvironment.isHeadless()) {
                monitor = null;
                log.warn("Unable to use progress monitor in headless mode!");
            } else {
                monitor = new ProgressMonitor();
                setPriority(NORM_PRIORITY - 1);
            }
        } else {
            monitor = null;
        }

        // Pointer to the offline history file
        eventRegistryFile = new File(configurator.getWorkDirectory(), "event-registry.txt");

        // Enable to remove remote events in gCal (hidden feature)
        deleteEnabled = configurator.getConfigProperty(Configurator.REMOTE_DELETE_ENABLED, true);
        if (!deleteEnabled) {
            log.info("Remote event removal disabled.");
        }

        // Init global static variables
        GCalUtilities.globalInit();
        ICalUtilities.globalInit();
        if (configurator.isFeedConverterEnabled()) {
            FeedUtilities.globalInit();
        }

        // Start synchronizer's thread
        start();
    }

    // --- CALENDAR CHANGE EVENT / ONLINE SYNCHRONIZER ---

    final synchronized void calendarChanged(CachedCalendar calendar) {
        changedCalendars.addLast(calendar);
        notifyAll();
    }

    // --- ONLINE SYNCHRONIZER LOOP ---

    public final void run() {
        for (;;) {
            try {

                // Wait for an enqued 'calendar chage event'
                CachedCalendar calendar;
                synchronized (this) {
                    while (changedCalendars.isEmpty()) {
                        wait();
                    }
                    calendar = (CachedCalendar) changedCalendars.removeFirst();
                }

                // Start synchronization
                log.debug("Starting Google Calendar synchronizer...");
                if (monitor != null) {
                    monitor.setVisible(true);
                }

                // Find new or changed events
                Calendar newCalendar = ICalUtilities.parseCalendar(calendar.body);
                Calendar oldCalendar = ICalUtilities.parseCalendar(calendar.previousBody);
                VEvent[] changes = ICalUtilities.getNewEvents(oldCalendar, newCalendar, true, calendar.url);
                if (changes.length == 0) {

                    // Find removed events (reverse method parameters)
                    changes = ICalUtilities.getNewEvents(newCalendar, oldCalendar, false, null);
                    if (changes.length != 0 && deleteEnabled) {

                        // Remove deleted events from Google
                        GCalUtilities.removeEvents(calendar, changes);
                    }
                } else {

                    // Get timezones
                    VTimeZone[] timeZones = ICalUtilities.getTimeZones(newCalendar);

                    // Update events in Google Calendar
                    GCalUtilities.updateEvents(calendar, timeZones, changes);
                }

                // Synchronization done
                log.debug("Synchronization finished.", null);
            } catch (InterruptedException interrupt) {
                return;
            } catch (Exception syncError) {
                if (monitor != null) {
                    try {
                        Toolkit.getDefaultToolkit().beep();
                    } catch (Throwable ignored) {
                    }
                }
                log.error("Unable to synchronize with Google Calendar!", syncError);
            } finally {

                // Hide progress monitor
                if (monitor != null) {
                    try {
                        monitor.setVisible(false);
                    } catch (Throwable ignored) {
                    }
                }
            }
        }
    }

    // --- ON DEMAND / OFFLINE SYNCHRONIZER ---

    final byte[] syncronizeNow(CachedCalendar calendar) throws Exception {
        log.debug("Starting Google Calendar synchronizer...");

        // Create processing variables
        boolean remoteEventChanged;
        Event entry;
        String uid, remoteUID;
        long remoteDate;
        Long storedDate;
        VEvent event;
        int i;

        // Load offline history
        loadEventRegistry();

        // Get historical parameters
        HashMap uids = (HashMap) eventRegistry.get(calendar.url);
        if (uids == null) {
            uids = new HashMap();
        }

        // Processed unique IDs
        HashSet processedUids = new HashSet();

        // Parse ics files
        Calendar localCalendar = ICalUtilities.parseCalendar(calendar.body);
        Calendar remoteCalendar = ICalUtilities.parseCalendar(calendar.previousBody);

        // Get local and remote changes
        VEvent[] localChanges = ICalUtilities.getNewEvents(remoteCalendar, localCalendar, true, calendar.url);
        VEvent[] remoteChanges = ICalUtilities.getNewEvents(localCalendar, remoteCalendar, false, null);

        // Updatable and removable events
        LinkedList insertableList = new LinkedList();
        LinkedList updatableList = new LinkedList();
        LinkedList removableList = new LinkedList();

        // Process local changes
        for (i = 0; i < localChanges.length; i++) {
            event = localChanges[i];
            uid = ICalUtilities.getUid(event);
            if (uid == null) {
                log.error("Invalid ical file (missing event ID)!");
                continue;
            }

            // Find remote pair
            entry = GCalUtilities.findEvent(calendar, event);
            if (entry == null) {
                if (uids.containsKey(uid)) {

                    // Event removed at Google side -> download & remove
                    if (log.isDebugEnabled()) {
                        log.debug("Removed event (" + ICalUtilities.getEventTitle(event)
                                + ") found in the Google Calendar.");
                    }
                } else {

                    // New local event -> insert
                    if (log.isDebugEnabled()) {
                        log.debug("New event (" + ICalUtilities.getEventTitle(event)
                                + ") found in the local calendar.");
                    }
                    insertableList.addLast(event);
                }
            } else {

                // Add local and remote ID to processed UIDs
                processedUids.add(entry.getId());

                // Get remote event's modification date
                remoteDate = entry.getUpdated().getValue();
                storedDate = (Long) uids.get(uid);
                remoteEventChanged = true;
                if (storedDate == null) {
                    remoteUID = GCalUtilities.getRemoteUID(calendar, uid);
                    if (remoteUID != null) {
                        storedDate = (Long) uids.get(remoteUID);
                    }
                }
                if (storedDate != null) {

                    // FIXME If a 'reminder' changes in GCal singly,
                    // Google Calendar does NOT update the LAST_MODIFIED
                    // timestamp. Otherwise this comparison works.
                    // there is no ms info in ics file
                    remoteEventChanged = storedDate.longValue() != remoteDate / 1000 * 1000;
                }
                if (remoteEventChanged) {

                    // Event modified at Google side -> download & update
                    if (log.isDebugEnabled()) {
                        log.debug("Updated event (" + ICalUtilities.getEventTitle(event)
                                + ") found in the Google Calendar.");
                    }
                } else {

                    // Local event modified -> update
                    if (log.isDebugEnabled()) {
                        log.debug("Updated event (" + ICalUtilities.getEventTitle(event)
                                + ") found in the local calendar.");
                    }
                    updatableList.addLast(event);
                }
            }
        }

        // Process remote changes
        for (i = 0; i < remoteChanges.length; i++) {
            event = remoteChanges[i];

            // Verify remote ID
            entry = GCalUtilities.findEvent(calendar, event);
            if (entry == null || processedUids.contains(entry.getId())) {
                continue;
            }

            // Verify local ID
            uid = ICalUtilities.getUid(event);
            if (uid == null) {
                log.error("Invalid ical file (missing event ID)!");
                continue;
            }

            // Find ID in history
            if (uids.containsKey(uid)) {

                // Local event removed -> remove event
                if (log.isDebugEnabled()) {
                    log.debug("Removed event (" + ICalUtilities.getEventTitle(event)
                            + ") found in the local calendar.");
                }
                removableList.addLast(event);
            } else {

                // New remote event -> download & create
                if (log.isDebugEnabled()) {
                    log.debug(
                            "New event (" + ICalUtilities.getEventTitle(event) + ") found in the Google Calendar.");
                }
            }
        }

        // Check changes
        if (localChanges.length == 0 && remoteChanges.length == 0) {

            // Save offline registry
            saveEventRegistry(calendar.url, calendar.previousBody);

            // Return previous body
            return calendar.previousBody;
        }

        // Show progress monitor
        if (monitor != null) {
            monitor.setVisible(true);
        }
        try {

            // Do modifications
            if (!removableList.isEmpty() && deleteEnabled) {

                // Remove Google entries
                VEvent[] events = new VEvent[removableList.size()];
                removableList.toArray(events);
                GCalUtilities.removeEvents(calendar, events);
            }
            VTimeZone[] timeZones;
            if (!updatableList.isEmpty() || !insertableList.isEmpty()) {

                // Get timezones
                timeZones = ICalUtilities.getTimeZones(localCalendar);
            } else {
                timeZones = new VTimeZone[0];
            }
            if (!updatableList.isEmpty()) {

                // Update Google entries
                VEvent[] events = new VEvent[updatableList.size()];
                updatableList.toArray(events);
                GCalUtilities.updateEvents(calendar, timeZones, events);
            }
            if (!insertableList.isEmpty()) {

                // Insert new Google entries
                VEvent[] events = new VEvent[insertableList.size()];
                insertableList.toArray(events);
                GCalUtilities.insertEvents(calendar, timeZones, events);
            }

            // Load new calendar from Google
            byte[] newBytes = GCalUtilities.loadCalendar(calendar);

            // Save offline registry
            saveEventRegistry(calendar.url, newBytes);

            // Return new ics file
            return newBytes;
        } finally {

            // Hide progress monitor
            if (monitor != null) {
                try {
                    monitor.setVisible(false);
                } catch (Throwable ignored) {
                }
            }
        }
    }

    // --- TODO I/O HANDLERS OF ON-DEMAND SYNC ---

    private final void loadEventRegistry() {
        if (!eventRegistry.isEmpty()) {
            return;
        }
        RandomAccessFile file = null;
        try {
            if (eventRegistryFile.isFile()) {

                // Load history file
                file = new RandomAccessFile(eventRegistryFile, "r");
                int len = (int) eventRegistryFile.length();
                byte[] bytes = new byte[len];
                file.readFully(bytes);
                file.close();
                String content = StringUtils.decodeToString(bytes, StringUtils.US_ASCII);
                bytes = null;

                // Parse history file
                StringTokenizer st = new StringTokenizer(content, "\r\n");
                HashMap uids = new HashMap();
                String urlHash = new String();
                String line;
                int i;
                while (st.hasMoreTokens()) {
                    line = st.nextToken().trim();
                    if (line.startsWith("URL\t")) {
                        if (!uids.isEmpty()) {
                            eventRegistry.put(urlHash, uids);
                        }
                        i = line.indexOf('\t');
                        if (i != -1) {
                            urlHash = line.substring(i + 1);
                            uids = new HashMap();
                        }
                        continue;
                    }
                    i = line.indexOf('\t');
                    if (i != -1) {
                        uids.put(line.substring(0, i), new Long(line.substring(i + 1)));
                    }
                }
                if (!uids.isEmpty()) {
                    eventRegistry.put(urlHash, uids);
                }
                if (log.isDebugEnabled()) {
                    log.debug("Event registry loaded successfully (" + len + " bytes).");
                }
                return;
            }
        } catch (Exception ioError) {
            if (file != null) {
                try {
                    file.close();
                    eventRegistryFile.delete();
                } catch (Exception ignored) {
                }
            }
            log.warn("Unable to load event registry!", ioError);
        }
    }

    private final void saveEventRegistry(String calendarURL, byte[] newBytes) throws Exception {

        // Verify ics file
        char[] chars = new char[Math.min(newBytes.length, 100)];
        int i;
        for (i = 0; i < chars.length; i++) {
            chars[i] = (char) newBytes[i];
        }
        if ((new String(chars)).indexOf(GCalUtilities.ERROR_MARKER) != -1) {
            return;
        }

        // Parse new ics file
        Calendar newCalendar = ICalUtilities.parseCalendar(newBytes);
        VEvent[] newEvents = ICalUtilities.getEvents(newCalendar);
        HashMap uids = new HashMap();
        VEvent event;
        String uid;
        for (i = 0; i < newEvents.length; i++) {
            event = newEvents[i];
            uid = ICalUtilities.getUid(event);
            if (uid != null) {
                uids.put(uid, new Long(event.getLastModified().getDateTime().getTime()));
            }
        }

        // Set historical parameters
        eventRegistry.put(calendarURL, uids);

        // Save file
        FileOutputStream out = null;
        try {

            // Create history
            QuickWriter writer = new QuickWriter();
            writer.write("#GCALDAEMON SYNCHRONIZER REGISTRY\r\n");
            writer.write("#PLEASE  DO NOT MODIFY THIS FILE!\r\n#");
            writer.write((new Date()).toString());
            writer.write("\r\n");

            // Write calendar uids
            Iterator keys = eventRegistry.keySet().iterator();
            Iterator iterator;
            String url;
            while (keys.hasNext()) {
                url = (String) keys.next();
                if (url == null || url.length() == 0) {
                    continue;
                }

                // Write path
                writer.write("\r\nURL\t");
                writer.write(url);
                writer.write("\r\n\r\n");

                // Write uids
                uids = (HashMap) eventRegistry.get(url);
                if (uids != null) {
                    iterator = uids.keySet().iterator();
                    while (iterator.hasNext()) {
                        uid = (String) iterator.next();
                        writer.write(uid);
                        writer.write('\t');
                        writer.write(((Long) uids.get(uid)).toString());
                        writer.write("\r\n");
                    }
                }
            }

            // Save content
            out = new FileOutputStream(eventRegistryFile);
            out.write(writer.getBytes());
            out.flush();
            out.close();
            if (log.isDebugEnabled()) {
                log.debug("Event registry saved successfully (" + writer.length() + " bytes).");
            }
        } catch (Exception ioError) {
            if (out != null) {
                try {
                    out.close();
                    eventRegistryFile.delete();
                } catch (Exception ignored) {
                }
            }
            log.warn("Unable to save event registry!", ioError);
        }
    }

}