org.gcaldaemon.core.GCalUtilities.java Source code

Java tutorial

Introduction

Here is the source code for org.gcaldaemon.core.GCalUtilities.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.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.TimeZone;

import net.fortuna.ical4j.data.CalendarOutputter;
import net.fortuna.ical4j.model.Calendar;
import net.fortuna.ical4j.model.Component;
import net.fortuna.ical4j.model.ComponentList;
import net.fortuna.ical4j.model.Date;
import net.fortuna.ical4j.model.Property;
import net.fortuna.ical4j.model.PropertyList;
import net.fortuna.ical4j.model.component.Observance;
import net.fortuna.ical4j.model.component.VEvent;
import net.fortuna.ical4j.model.component.VTimeZone;
import net.fortuna.ical4j.model.property.CalScale;
import net.fortuna.ical4j.model.property.Description;
import net.fortuna.ical4j.model.property.ProdId;
import net.fortuna.ical4j.model.property.TzId;
import net.fortuna.ical4j.model.property.TzOffsetTo;
import net.fortuna.ical4j.model.property.Uid;
import net.fortuna.ical4j.model.property.Version;

import org.apache.commons.httpclient.Credentials;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
import org.apache.commons.httpclient.UsernamePasswordCredentials;
import org.apache.commons.httpclient.auth.AuthScope;
import org.apache.commons.httpclient.params.HttpConnectionManagerParams;
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;
import com.google.gdata.data.DateTime;
import com.google.gdata.data.extensions.Recurrence;
import com.google.gdata.util.ServiceException;

/**
 * Google Calendar utilities.
 * 
 * <li>loadCalendar <li>updateEvents <li>removeEvents
 * 
 * Created: Jan 03, 2007 12:50:56 PM
 * 
 * @author Andras Berkes
 */
public final class GCalUtilities {

    // --- CONSTANTS ---

    public static final String ERROR_MARKER = "gcaldaemon-error";

    private static final long GOOGLE_CONNECTION_TIMEOUT = 1000L * 60 * 5;
    private static final long GOOGLE_RETRY_MILLIS = 1000L;
    private static final int HTTP_CONNECTION_TIMEOUT = 10000;
    private static final int HTTP_WAIT_TIMEOUT = 60000;

    private static final int MAX_POOLED_CONNECTIONS = 100;
    private static final int MAX_FEED_ENTRIES = 10000;

    private static final String GOOGLE_HTTPS_URL = "https://www.google.com";
    private static final String GOOGLE_HTTP_URL = "http://www.google.com";
    private static final String CALENDAR_FEED_POSTFIX = "/private/full";
    private static final String USER_AGENT = "Mozilla/5.0 (Windows; U;"
            + " Windows NT 5.1; hu; rv:1.8.0.8) Gecko/20061025 Thunderbird/1.5.0.8";
    private static final String CALENDAR_FEED_PREFIX = GOOGLE_HTTPS_URL + "/calendar/feeds/";
    private static final String METAFEED_URL = CALENDAR_FEED_PREFIX + "default";
    private static final String FEEDS_DEFAULT_PART = "/feeds/default/";
    private static final String CALENDAR_ICAL_PART = "/calendar/ical/";
    private static final String PRIVATE_BASIC_PART = "/private/basic.ics";

    private static final String UID_EXTENSION_NAME = "gcaldaemon-uid";
    private static final String CATEGORIES_EXTENSION_NAME = "gcaldaemon-categories";
    private static final String PRIORITY_EXTENSION_NAME = "gcaldaemon-priority";
    private static final String URL_EXTENSION_NAME = "gcaldaemon-url";

    private static final char[] CR_LF = "\r\n".toCharArray();
    private static final char[] ALARM_BEGIN = "\r\nBEGIN:VALARM\r\nTRIGGER;VALUE=DURATION:-P".toCharArray();
    private static final char[] ALARM_END = "\r\nACTION:AUDIO\r\nEND:VALARM\r\n".toCharArray();
    private static final char[] ALARM_MOZ_LASTACK = "\r\nX-MOZ-LASTACK:".toCharArray();
    private static final char[] ALARM_RAIN_LASTACK = "X-RAINLENDAR-LASTALARMACK:".toCharArray();

    private static final long LAST_ACK_TIMEOUT = 86400000L;

    private static final TimeZone UTC = TimeZone.getTimeZone("UTC");

    // --- LOGGER ---

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

    // --- GLOBAL PROPERTIES ---

    private static boolean enableExtensions;
    private static boolean sendInvitations;
    private static boolean enableEmail;
    private static boolean enableSms;
    private static boolean enablePopup;

    // --- HTTP CONNECTION HANDLER ---

    private static final MultiThreadedHttpConnectionManager connectionManager = new MultiThreadedHttpConnectionManager();
    private static final HttpClient httpClient = new HttpClient(connectionManager);

    static final void globalInit() {
        try {

            // Set extended sync mode
            String value = System.getProperty("gcaldaemon.extended.sync", "false");
            enableExtensions = "true".equals(value);

            // Set send invitations
            value = System.getProperty("gcaldaemon.send.invitations", "false");
            sendInvitations = "true".equals(value);

            // Set enabled alarm types in the Google Calendar
            value = System.getProperty("gcaldaemon.remote.alarms", "email,sms,popup");
            boolean email = value.indexOf("mail") != -1;
            boolean sms = value.indexOf("sms") != -1;
            boolean popup = value.indexOf("pop") != -1;
            if (!email && !sms && !popup) {
                enableEmail = true;
                enableSms = true;
                enablePopup = true;
            } else {
                enableEmail = email;
                enableSms = sms;
                enablePopup = popup;
            }

            // Set proxy
            HttpConnectionManagerParams params = connectionManager.getParams();
            params.setConnectionTimeout(HTTP_CONNECTION_TIMEOUT);
            params.setSoTimeout(HTTP_WAIT_TIMEOUT);
            String proxyHost = System.getProperty("http.proxyHost");
            String proxyPort = System.getProperty("http.proxyPort");
            if (proxyHost != null && proxyPort != null) {
                httpClient.getHostConfiguration().setProxy(proxyHost, Integer.parseInt(proxyPort));
                String username = System.getProperty("http.proxyUserName");
                String password = System.getProperty("http.proxyPassword");
                if (username != null && password != null) {
                    Credentials credentials = new UsernamePasswordCredentials(username, password);
                    httpClient.getState().setProxyCredentials(AuthScope.ANY, credentials);
                }
            }
        } catch (Exception setupError) {
            log.warn("Unable to init proxy!", setupError);
        }
    }

    // --- PRIVATE CONSTRUCTOR ---

    private GCalUtilities() {
    }

    // --- GOOGLE ICALENDAR LOADER ---

    static final byte[] loadCalendar(Request request) throws Exception {
        return GCalUtilitiesV3.loadCalendar(request);
    }

    public static final byte[] exceptionToCalendar(Exception loadError) throws Exception {

        // Create new calendar
        Calendar calendar = new Calendar();
        PropertyList props = calendar.getProperties();
        props.add(new ProdId(ERROR_MARKER));
        props.add(Version.VERSION_2_0);
        props.add(CalScale.GREGORIAN);

        // Convert exception to event
        String title, content;
        if (loadError != null && loadError instanceof UnknownHostException) {
            title = "NETWORK DOWN";
            content = "Service temporarily unavailable!\r\n" + "Please do not modify this calendar! "
                    + "Try clicking on the Reload or Refresh button. " + "If this doesn't work, try again later.";
        } else {
            title = "UNAVAILABLE";
            content = "Service unavailable!\r\n" + "Please do not modify this calendar!";
        }
        long eventStart = System.currentTimeMillis();
        long eventEnd = eventStart + 2700000L;
        VEvent event = new VEvent(new net.fortuna.ical4j.model.DateTime(eventStart),
                new net.fortuna.ical4j.model.DateTime(eventEnd), title);

        // Generate UID by start millis
        PropertyList args = event.getProperties();
        Uid uid = new Uid(ERROR_MARKER);
        args.add(uid);

        // Create description
        if (loadError != null) {
            String message = loadError.getMessage();
            if (message != null && message.length() != 0) {
                content = content + "\r\n[cause: " + message + ']';
            }
        }
        Description desc = new Description(content);
        args.add(desc);

        // Add marker event to calendar
        ComponentList events = calendar.getComponents();
        events.add(event);

        // Get calendar bytes
        ByteArrayOutputStream buffer = new ByteArrayOutputStream(1024);
        CalendarOutputter outputter = new CalendarOutputter();
        outputter.output(calendar, buffer);
        return buffer.toByteArray();
    }

    // --- AUTOMATIC TIME ZONE MANAGEMENT ---

    private static final HashSet registeredTimeZones = new HashSet();

    private static final void registerTimeZones(String content, byte[] bytes) {
        try {
            StringTokenizer st = new StringTokenizer(content, "\r\n");
            HashSet timeZones = new HashSet();
            String line, timeZone;
            while (st.hasMoreTokens()) {
                line = st.nextToken();
                if (!line.startsWith("TZID:")) {
                    continue;
                }
                timeZone = line.substring(5);
                if (timeZone.length() == 0) {
                    continue;
                }
                if (registeredTimeZones.contains(timeZone)) {
                    continue;
                }
                timeZones.add(timeZone);
            }
            if (timeZones.isEmpty()) {
                return;
            }
            Calendar calendar = ICalUtilities.parseCalendar(bytes);
            VTimeZone[] zones = ICalUtilities.getTimeZones(calendar);
            if (zones.length == 0) {
                return;
            }
            Component seasonalTime;
            TzOffsetTo offsetTo;
            String id, offset;
            VTimeZone zone;
            for (int i = 0; i < zones.length; i++) {
                zone = zones[i];
                seasonalTime = zone.getObservances().getComponent(Observance.STANDARD);
                if (seasonalTime == null) {
                    seasonalTime = zone.getObservances().getComponent(Observance.DAYLIGHT);
                }
                id = zone.getTimeZoneId().getValue();
                if (registeredTimeZones.contains(id)) {
                    continue;
                }
                if (seasonalTime == null) {
                    continue;
                }
                offsetTo = (TzOffsetTo) seasonalTime.getProperty(Property.TZOFFSETTO);
                if (offsetTo == null) {
                    continue;
                }
                registeredTimeZones.add(id);
                offset = offsetTo.getValue();
                log.debug("Set the offset of " + id + " to GMT" + offset + ".");
                if (!ICalUtilities.setTimeZone(id, offset)) {
                    log.warn("Unknown time zone (" + id + ")!");
                }
            }
        } catch (Exception ignored) {
            log.debug(ignored);
        }
    }

    // --- EVENT FINDER ---

    static final Event findEvent(CachedCalendar calendar, VEvent event) throws Exception {
        return GCalUtilitiesV3.findEvent(calendar, event);
    }

    // --- EVENT CREATOR ---

    static final void insertEvents(CachedCalendar calendar, VTimeZone[] timeZones, VEvent[] events)
            throws Exception {
        GCalUtilitiesV3.insertEvents(calendar, timeZones, events);
    }

    private static final String getMessageBody(Exception exception) {
        if (exception == null) {
            return "";
        }
        String body = null;
        if (exception instanceof ServiceException) {
            body = ((ServiceException) exception).getResponseBody();
        }
        if (body == null || body.length() == 0) {
            body = exception.toString();
        }
        return body;
    }

    // --- EVENT UPDATER ---

    static final void updateEvents(CachedCalendar calendar, VTimeZone[] timeZones, VEvent[] events)
            throws Exception {
        GCalUtilitiesV3.updateEvents(calendar, timeZones, events);
    }

    private static final URL getFeedURL(Request request) throws Exception {
        String target = request.url;
        int i = target.indexOf("/ical/");
        if (i == -1) {
            throw new Exception("Malformed iCal URL, '/ical/' part not found: " + request.url);
        }
        target = target.substring(i + 6);
        i = target.indexOf('/');
        if (i == -1) {
            throw new Exception("Malformed iCal URL, 4th '/' character not found: " + request.url);
        }
        target = target.substring(0, i);
        return new URL(CALENDAR_FEED_PREFIX + target + CALENDAR_FEED_POSTFIX);
    }

    // --- EVENT REMOVER ---

    static final void removeEvents(CachedCalendar calendar, VEvent[] events) throws Exception {
        GCalUtilitiesV3.removeEvents(calendar, events);
    }

    // --- ICAL EVENT TO GOOGLE EVENT CONVERTER ---

    static final DateTime toDateTime(Date date) throws Exception {
        if (date == null) {
            return null;
        }
        boolean isAllDay = date.toString().indexOf('T') == -1;
        DateTime dateTime;
        if (isAllDay) {
            dateTime = toOneDayEventDateTime(date);
        } else {
            dateTime = new DateTime(date, UTC);
        }
        dateTime.setDateOnly(isAllDay);
        return dateTime;
    }

    private static final DateTime toOneDayEventDateTime(Date date) throws Exception {

        // Convert one day event's date to UTC date
        String text = date.toString();
        GregorianCalendar calendar = new GregorianCalendar(UTC);
        calendar.set(GregorianCalendar.YEAR, Integer.parseInt(text.substring(0, 4)));
        calendar.set(GregorianCalendar.MONTH, Integer.parseInt(text.substring(4, 6)) - 1);
        calendar.set(GregorianCalendar.DAY_OF_MONTH, Integer.parseInt(text.substring(6)));
        calendar.set(GregorianCalendar.HOUR_OF_DAY, 0);
        calendar.set(GregorianCalendar.MINUTE, 0);
        calendar.set(GregorianCalendar.SECOND, 0);
        calendar.set(GregorianCalendar.MILLISECOND, 0);
        DateTime dateTime = new DateTime(calendar.getTime(), UTC);
        return dateTime;
    }

    private static final VTimeZone getRecurrenceTimeZone(VTimeZone[] timeZones, VEvent event) throws Exception {
        if (timeZones == null || timeZones.length == 0) {
            return null;
        }
        String tzid = getTimeZoneID(event);
        if (tzid != null) {
            VTimeZone timeZone;
            for (int i = 0; i < timeZones.length; i++) {
                timeZone = timeZones[i];
                TzId id = timeZone.getTimeZoneId();
                if (tzid.toLowerCase().equals(id.getValue().toString().toLowerCase())) {
                    return timeZone;
                }
            }
        }
        return null;
    }

    private static final String getTimeZoneID(VEvent event) throws Exception {
        Property start = event.getProperty(Property.DTSTART);
        if (start != null) {
            String tzid = start.toString();
            if (tzid != null) {
                int s = tzid.indexOf(Property.TZID);
                if (s != -1) {
                    int e = tzid.indexOf(':', s);
                    if (e != -1) {
                        return tzid.substring(s + 5, e);
                    } else {
                        return null;
                    }
                } else {
                    return null;
                }
            }
        }
        return null;
    }

    // --- GOOGLE EVENT FEED ---

    // --- EVENT FINDER ---

    private static final HashMap editURLMaps = new HashMap();
    private static final HashMap uidMaps = new HashMap();

    static final String getRemoteUID(CachedCalendar calendar, String id) {
        HashMap mappedUIDs = (HashMap) uidMaps.get(calendar.url);
        if (mappedUIDs == null) {
            return null;
        }
        return (String) mappedUIDs.get(id);
    }

    private static final VEvent parseRecurrence(Recurrence recurrence) {
        if (recurrence == null) {
            return null;
        }
        VEvent event = null;
        try {
            QuickWriter writer = new QuickWriter(300);
            writer.write("BEGIN:VCALENDAR\r\n");
            writer.write("VERSION:2.0\r\n");
            writer.write("PRODID:DUMMY\r\n");
            writer.write("CALSCALE:GREGORIAN\r\n");
            writer.write("BEGIN:VEVENT\r\n");
            writer.write("UID:DUMMY\r\n");
            writer.write("SUMMARY:DUMMY\r\n");
            writer.write(recurrence.getValue());
            writer.write("\r\nEND:VEVENT\r\n");
            writer.write("END:VCALENDAR\r\n");
            Calendar calendar = ICalUtilities.parseCalendar(writer.getBytes());
            return ICalUtilities.getEvents(calendar)[0];
        } catch (Exception ignored) {
            log.debug(ignored);
        }
        return event;
    }

    // --- GOOGLE CONNECTION POOL ---

    private static final HashMap servicePool = new HashMap();
    private static final HashSet invalidCredentials = new HashSet();

    public static final boolean hasInvalidCredentials(Request request) {
        return invalidCredentials.remove(request.url + '\t' + request.username + '\t' + request.password);
    }

    public static final String normalizeUsername(String username) {
        if (username != null && username.length() > 0) {
            if (username.endsWith("@googlemail.com") || username.endsWith("@gmail")
                    || username.endsWith("@googlemail")) {
                return username.substring(0, username.indexOf('@')) + "@gmail.com";
            }
            if (username.indexOf('@') == -1) {
                log.warn("Malformed username (" + username + "@where)!");
            }
        }
        return username;
    }

    // --- LIST CALENDARS ---

    private static final Properties calendarNames = new Properties();

    public static final String[] getCalendarURLs(Request request, File workDir) throws Exception {
        return GCalUtilitiesV3.getCalendarURLs(request, workDir);
    }

    public static final String getCalendarName(String url, File workDir) {
        if (url == null || url.length() == 0) {
            return null;
        }
        if (calendarNames.isEmpty()) {
            loadCalendarNamesFromCache(workDir);
        }
        if (url.startsWith(GOOGLE_HTTP_URL)) {
            url = url.substring(GOOGLE_HTTP_URL.length());
        } else {
            if (url.startsWith(GOOGLE_HTTPS_URL)) {
                url = url.substring(GOOGLE_HTTPS_URL.length());
            }
        }
        String name = (String) calendarNames.get(url);
        if (name != null) {
            return name;
        }
        int i = url.indexOf("/private");
        if (i != -1) {
            url = url.substring(0, i);
            Iterator names = calendarNames.entrySet().iterator();
            Map.Entry entry;
            while (names.hasNext()) {
                entry = (Map.Entry) names.next();
                if (((String) entry.getKey()).startsWith(url)) {
                    return (String) entry.getValue();
                }
            }
        }
        return null;
    }

    private static final void loadCalendarNamesFromCache(File workDir) {
        try {
            File file = new File(workDir, "gcal-names.txt");
            if (!file.isFile()) {
                return;
            }
            BufferedInputStream in = new BufferedInputStream(new FileInputStream(file));
            calendarNames.load(in);
            in.close();
        } catch (Exception ioException) {
            log.warn("Unable to load 'gcal-names.txt'!", ioException);
        }
    }

    private static final void saveCalendarNamesToCache(File workDir) {
        try {
            File file = new File(workDir, "gcal-names.txt");
            BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(file));
            calendarNames.store(out, "CALENDAR NAME CACHE");
            out.flush();
            out.close();
        } catch (Exception ioException) {
            log.warn("Unable to save 'gcal-names.txt'!", ioException);
        }
    }

}