org.gcaldaemon.api.SyncEngine.java Source code

Java tutorial

Introduction

Here is the source code for org.gcaldaemon.api.SyncEngine.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.api;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.RandomAccessFile;
import java.net.URL;
import java.util.Properties;

import org.gcaldaemon.core.CachedCalendar;
import org.gcaldaemon.core.Configurator;
import org.gcaldaemon.core.GCalUtilities;
import org.gcaldaemon.core.Request;
import org.gcaldaemon.core.StringUtils;
import org.gcaldaemon.logger.QuickWriter;

/**
 * Embeddable synchronizer engine for Google Calendar. Examples:<br>
 * <br>
 * 
 * File workDir = new File("/etc/work");<br>
 * File localCalendar = new File("/etc/calendar.ics");<br>
 * URL remoteCalendar = new URL("http://www.google.com/private ical url");<br>
 * String username = "user@gmail.com";<br>
 * String password = "gmailpassword";<br>
 * <br>
 * 
 * SyncEngine engine = new SyncEngine(workDir);<br>
 * engine.synchronize(localCalendar, remoteCalendar, username, password);<br>
 * <br>
 * 
 * or<br>
 * <br>
 * 
 * PDAConnection pda = new PDAConnection("COM3");<br>
 * File workDir = new File("/etc/work");<br>
 * byte[] icalBytes = pda.loadCalendar();<br>
 * URL remoteCalendar = new URL("http://www.google.com/private ical url");<br>
 * String username = "user@gmail.com";<br>
 * String password = "gmailpassword";<br>
 * <br>
 * 
 * SyncEngine engine = new SyncEngine(workDir);<br>
 * icalBytes = engine.synchronize(icalBytes, remoteCalendar, username,
 * password);<br>
 * pda.saveCalendar(icalBytes);<br>
 * <br>
 * 
 * WARNING: SyncEngine is NOT a thread-safe object, so web components such as
 * Servlets and JSPs should avoid injecting it and storing it as a shared field.
 * If you wish to use SyncEngine in a multithreaded environment, you will have
 * to create a static synchronization object or a static synchronization block
 * to manage a cached instance.<br>
 * <br>
 * 
 * SyncEngine utilizes the logging interface provided by the Commons Logging
 * package. Commons Logging provides a simple and generalized log interface to
 * various logging packages. By using Commons Logging, SyncEngine can be
 * configured for a variety of different logging behaviours. That means the
 * developer will have to make a choice which logging framework to use. To
 * specify a specific logger be used, set this system property:<br>
 * <br>
 * 
 * org.apache.commons.logging.Log<br>
 * <br>
 * 
 * to one of:<br>
 * <li>org.apache.commons.logging.impl.SimpleLog
 * <li>org.apache.commons.logging.impl.AvalonLogger
 * <li>org.apache.commons.logging.impl.Jdk13LumberjackLogger
 * <li>org.apache.commons.logging.impl.Jdk14Logger
 * <li>org.apache.commons.logging.impl.Log4JLogger
 * <li>org.apache.commons.logging.impl.LogKitLogger
 * <li>org.apache.commons.logging.impl.NoOpLog
 * <li>org.gcaldaemon.logger.DefaultLog<br>
 * 
 * By default, the SyncEngine will use the DefaultLog framework. DefaultLog is a
 * simple implementation of the Log interface that sends all log messages to
 * "System.out". Configuration example:
 * System.setProperty("org.apache.commons.logging.Log",
 * "org.apache.commons.logging.impl.Jdk14Logger");
 * 
 * Created: Jan 22, 2008 12:50:56 PM
 * 
 * @author Andras Berkes
 */
public final class SyncEngine {

    // --- CONSTANTS ---

    private static final String GOOGLE_HTTPS_URL = "https://www.google.com";

    // --- INTERNAL VARIABLES ---

    /**
     * Contains the engine's registry, ToDo items, and backups.
     */
    private File workDir;

    /**
     * Container of the engine's configuration.
     */
    private Properties properties;

    /**
     * Internal configurator and calendar synchronizer utility (cached
     * instance).
     */
    private Configurator configurator;

    /**
     * Property that indicates the properties in the engine's configuration have
     * been changed.
     */
    private boolean configChanged;

    // --- CONSTRUCTORS ---

    /**
     * Default constructor, the SyncEngine will use 'USER_HOME/.gcaldaemon'
     * folder as working directory.
     * 
     * @throws FileNotFoundException
     *             unable to open or create the 'USER_HOME/.gcaldaemon'
     *             directory (file permission problem)
     */
    public SyncEngine() throws FileNotFoundException {

        // 'null' means 'USER_HOME/.gcaldaemon'
        this(null);
    }

    /**
     * Creates a SyncEngine instance with the given working directory. Google
     * Calendar - unlike Sunbird/Lightning - does not support ToDo (Task) items.
     * Therefore GCALDaemon stores ToDo items in a local file storage. This
     * storage is the 'working directory'.
     * 
     * @param workDir
     *            working directory (contains the engine's registry, ToDo items,
     *            and backups)
     * 
     * @throws FileNotFoundException
     *             unable to open or create the specified working directory
     *             (file permission problem)
     */
    public SyncEngine(File workDir) throws FileNotFoundException {

        // Init working directory
        if (workDir == null) {
            String home = System.getProperty("user.home");
            if (home == null || home.length() == 0) {
                home = "/";
            }
            workDir = new File(home, ".gcaldaemon");
        }
        if (!workDir.isDirectory() && !workDir.mkdirs()) {

            // File permission problem
            throw new FileNotFoundException(String.valueOf(workDir));
        }
        this.workDir = workDir;
        String workDirPath = workDir.getAbsolutePath();
        properties = new Properties();
        configChanged = true;

        // Set default engine properties (synchronizer)
        properties.put(Configurator.WORK_DIR, workDirPath);
        properties.put(Configurator.CACHE_TIMEOUT, "180000");
        properties.put(Configurator.HTTP_ENABLED, "false");
        properties.put(Configurator.PROGRESS_ENABLED, "false");
        properties.put(Configurator.SEND_INVITATIONS, "false");
        properties.put(Configurator.ICAL_BACKUP_TIMEOUT, "604800000");
        properties.put(Configurator.EXTENDED_SYNC_ENABLED, "true");
        properties.put(Configurator.REMOTE_ALARM_TYPES, "email,sms,popup");

        // Set default engine properties (RSS/ATOM feed converter)
        try {

            // Verify classpath
            Class.forName("com.sun.syndication.io.SyndFeedInput");

            // Enable feed converter
            properties.put(Configurator.FEED_ENABLED, "true");
            properties.put(Configurator.FEED_CACHE_TIMEOUT, "3600000");
            properties.put(Configurator.FEED_EVENT_LENGTH, "2700000");
            properties.put(Configurator.FEED_DUPLICATION_FILTER, "70");
        } catch (Throwable ignored) {

            // Disable feed converter
            properties.put(Configurator.FEED_ENABLED, "false");
        }

        // Init default logger (DefaultLog). DefaultLog is an implementation of
        // the Log interface that sends all enabled log messages, for all
        // defined loggers, to System.out.
        if (System.getProperty("org.apache.commons.logging.Log") == null) {

            // You can override this setting with the
            // "org.apache.commons.logging.Log" system property.
            // Available log factories:
            // 
            // 1) org.apache.commons.logging.impl.SimpleLog
            // 2) org.apache.commons.logging.impl.AvalonLogger
            // 3) org.apache.commons.logging.impl.Jdk13LumberjackLogger
            // 4) org.apache.commons.logging.impl.Jdk14Logger
            // 5) org.apache.commons.logging.impl.Log4JLogger
            // 6) org.apache.commons.logging.impl.LogKitLogger
            // 7) org.apache.commons.logging.impl.NoOpLog
            // 8) org.gcaldaemon.logger.DefaultLog
            // 
            System.setProperty("org.apache.commons.logging.Log", "org.gcaldaemon.logger.DefaultLog");
        }
    }

    // --- GOOGLE CALENDAR INFO ---

    /**
     * Lists Google Calendars in the account specified by the given
     * username/password.
     * 
     * @param username
     *            full name of the user (eg. "username@gmail.com" or
     *            "username@mydomain.org")
     * @param password
     *            Gmail password (in unencrypted, plain text format)
     * 
     * @return array of the remote calendars
     * 
     * @throws Exception
     *             any exception (eg. i/o, invalid param, invalid password)
     * 
     * @see #loadCalendar
     * @see #synchronize
     */
    public final RemoteCalendar[] listCalendars(String username, String password) throws Exception {

        // Verify required parameters
        if (username == null || username.length() == 0) {
            throw new NullPointerException("username = null");
        }
        if (username.indexOf('@') == -1) {
            throw new IllegalArgumentException("invalid username");
        }
        if (password == null || password.length() == 0) {
            throw new NullPointerException("password = null");
        }

        // Create (or reinitialize) the cached instance
        if (configChanged) {
            configChanged = false;
            if (configurator != null) {
                configurator.interrupt();
            }
            configurator = new Configurator(null, properties, false, Configurator.MODE_EMBEDDED);
        }

        // Create request container
        Request request = new Request();
        request.username = username;
        request.password = password;

        // Load paths
        String[] paths = GCalUtilities.getCalendarURLs(request, workDir);

        // Convert to RemoteCalendar array
        RemoteCalendar[] array = new RemoteCalendar[paths.length];
        String path;
        URL url;
        for (int i = 0; i < paths.length; i++) {
            path = paths[i];
            url = new URL(GOOGLE_HTTPS_URL + path);
            array[i] = new RemoteCalendar(GCalUtilities.getCalendarName(path, workDir), url);
        }
        return array;
    }

    // --- GOOGLE CALENDAR LOADER / FEED CONVERTER ---

    /**
     * Downloads a specified RSS/ATOM feed and converts it to iCalendar (ICS)
     * format (or gets from the calendar cache).
     * 
     * @param feedURL
     *            RSS/ATOM feed's URL (eg.
     *            "http://newsrss.bbc.co.uk/rss/newsonline_uk_edition/sci/tech/rss.xml")
     * 
     * @return the converted content of the feed in iCalendar format
     * 
     * @throws Exception
     *             any exception (eg. i/o, invalid param, etc)
     * 
     * @see #getCacheTimeout
     * @see #setCacheTimeout
     */
    public final byte[] loadCalendar(URL feedURL) throws Exception {
        return synchronize((byte[]) null, feedURL, null, null);
    }

    /**
     * Downloads a specified Google Calendar (with ToDo entries) in iCalendar
     * (ICS) format, or gets from the calendar cache).
     * 
     * @param remoteCalendar
     *            Google Calendar's private ICAL URL
     *            ("https://www.google.com/calendar/ical/.../basic.ics"), or the
     *            RSS/ATOM feed's URL (= feed converter mode)
     * @param username
     *            full name of the user (eg. "username@gmail.com" or
     *            "username@mydomain.org"), this value is optional in feed
     *            converter mode
     * @param password
     *            Gmail password (in unencrypted, plain text format), this value
     *            is optional in feed converter mode
     * 
     * @return the new, synchronized content of the calendar in iCalendar format
     * 
     * @throws Exception
     *             any exception (eg. i/o, invalid param, invalid calendar
     *             syntax, etc)
     * 
     * @see #getCacheTimeout
     * @see #setCacheTimeout
     */
    public final byte[] loadCalendar(URL remoteCalendar, String username, String password) throws Exception {
        return synchronize((byte[]) null, remoteCalendar, username, password);
    }

    // --- MAIN SYNCHRONIZER / FEED CONVERTER METHODS ---

    /**
     * Synchronizes a remote Google Calendar to a local iCalendar (RFC 2445)
     * file. If the local calendar file does not exists, the SyncEngine will
     * download and save the original iCalendar file (with ToDo entries) without
     * any synchronization. Creates daily backups of all Google AND local
     * calendars into the 'backup' subdirectory (under the working directory).
     * 
     * @param localCalendar
     *            local calendar file
     * @param remoteCalendar
     *            Google Calendar's private ICAL URL
     *            ("https://www.google.com/calendar/ical/.../basic.ics"), or the
     *            RSS/ATOM feed's URL (= feed converter mode)
     * @param username
     *            full name of the user (eg. "username@gmail.com" or
     *            "username@mydomain.org"), this value is optional in feed
     *            converter mode
     * @param password
     *            Gmail password (in unencrypted, plain text format), this value
     *            is optional in feed converter mode
     * 
     * @throws Exception
     *             any exception (eg. i/o, invalid param, invalid calendar
     *             syntax, etc)
     * 
     * @see #getCacheTimeout
     * @see #setCacheTimeout
     */
    public final void synchronize(File localCalendar, URL remoteCalendar, String username, String password)
            throws Exception {

        // Verify required parameters
        if (localCalendar == null) {
            throw new NullPointerException("localCalendar = null");
        }
        if (remoteCalendar == null) {
            throw new NullPointerException("remoteCalendar = null");
        }
        String path = remoteCalendar.getPath();
        if (path.endsWith(".ics")) {

            // Synchronizer mode
            if (username == null || username.length() == 0) {
                throw new NullPointerException("username = null");
            }
            if (username.indexOf('@') == -1) {
                throw new IllegalArgumentException("invalid username");
            }
            if (password == null || password.length() == 0) {
                throw new NullPointerException("password = null");
            }
        } else {

            // Feed converter mode
            checkFeedConverter();
            path = remoteCalendar.toString();
        }

        // Load local calendar file
        byte[] bytes = null;
        if (localCalendar.isFile()) {
            RandomAccessFile file = null;
            try {
                file = new RandomAccessFile(localCalendar, "r");
                bytes = new byte[(int) localCalendar.length()];
                file.readFully(bytes);
            } finally {
                if (file != null) {
                    file.close();
                }
            }
        }

        // Create (or reinitialize) the cached instance
        if (configChanged) {
            configChanged = false;
            configurator = new Configurator(null, properties, false, Configurator.MODE_EMBEDDED);
        }

        // Create request container
        Request request = new Request();
        request.body = bytes;
        request.url = path;
        request.username = username;
        request.password = password;
        request.filePath = localCalendar.getAbsolutePath();

        // Do synchronization (if the 'localCalendar' is defined)
        if (bytes != null && bytes.length != 0) {
            configurator.synchronizeNow(request);
        }

        // Return the modified calendar (with ToDo entries)
        CachedCalendar calendar = configurator.getCalendar(request);
        bytes = calendar.toByteArray();

        // Save new content into the calendar file
        FileOutputStream out = null;
        try {
            out = new FileOutputStream(localCalendar);
            out.write(bytes);
            out.flush();
        } finally {
            if (out != null) {
                out.close();
            }
        }
    }

    /**
     * Synchronizes a remote Google Calendar to a local iCalendar specified as
     * an iCalendar (ICS) byte array. Creates daily backups of all Google
     * Calendars into the 'backup' subdirectory (under the working directory).
     * 
     * @param localCalendar
     *            bytes of the local calendar in iCalendar format (or null =
     *            downloads calendar without any synchronization, or returns
     *            from the calendar cache)
     * @param remoteCalendar
     *            Google Calendar's private ICAL URL
     *            ("https://www.google.com/calendar/ical/.../basic.ics"), or the
     *            RSS/ATOM feed's URL (= feed converter mode)
     * @param username
     *            full name of the user (eg. "username@gmail.com" or
     *            "username@mydomain.org"), this value is optional in feed
     *            converter mode
     * @param password
     *            Gmail password (in unencrypted, plain text format), this value
     *            is optional in feed converter mode
     * 
     * @return the new, synchronized content of the calendar, in iCalendar
     *         format
     * 
     * @throws Exception
     *             any exception (eg. i/o, invalid param, invalid calendar
     *             syntax, etc)
     * 
     * @see #getCacheTimeout
     * @see #setCacheTimeout
     */
    public final byte[] synchronize(byte[] localCalendar, URL remoteCalendar, String username, String password)
            throws Exception {

        // Verify required parameters
        if (remoteCalendar == null) {
            throw new NullPointerException("remoteCalendar = null");
        }
        String path = remoteCalendar.getPath();
        if (path.endsWith(".ics")) {

            // Synchronizer mode
            if (username == null || username.length() == 0) {
                throw new NullPointerException("username = null");
            }
            if (username.indexOf('@') == -1) {
                throw new IllegalArgumentException("invalid username");
            }
            if (password == null || password.length() == 0) {
                throw new NullPointerException("password = null");
            }
        } else {

            // Feed converter mode
            checkFeedConverter();
            path = remoteCalendar.toString();
        }

        // Create (or reinitialize) the cached instance
        if (configChanged) {
            configChanged = false;
            configurator = new Configurator(null, properties, false, Configurator.MODE_EMBEDDED);
        }

        // Create request container
        Request request = new Request();
        request.body = localCalendar;
        request.url = path;
        request.username = username;
        request.password = password;

        // Do synchronization (if the 'localCalendar' is defined)
        if (localCalendar != null && localCalendar.length != 0) {
            configurator.synchronizeNow(request);
        }

        // Return the modified calendar (with ToDo entries)
        CachedCalendar calendar = configurator.getCalendar(request);
        return calendar.toByteArray();
    }

    // --- PRIVATE PROPERTY GETTERS/SETTERS ---

    /**
     * Puts a String property into the engine's configuration.
     * 
     * @param key
     *            name of the config property
     * @param value
     *            value of the config property
     */
    private final void setConfigProperty(String key, String value) {

        // Compare to the previous value
        String previous = properties.getProperty(key, "");
        if (previous.equals(value)) {
            return;
        }

        // Set new value
        properties.setProperty(key, value);
        configChanged = true;
    }

    /**
     * Puts a boolean property into the engine's configuration.
     * 
     * @param key
     *            name of the config property
     * @param value
     *            value of the config property
     * 
     * @see #setConfigProperty
     */
    private final void setConfigProperty(String key, boolean value) {
        setConfigProperty(key, Boolean.toString(value));
    }

    /**
     * Puts a numeric (long) property into the engine's configuration.
     * 
     * @param key
     *            name of the config property
     * @param value
     *            value of the config property
     * 
     * @see #setConfigProperty
     */
    private final void setConfigProperty(String key, long value) {
        setConfigProperty(key, Long.toString(value));
    }

    /**
     * Sets the value of the 'remote.alarm.types' property.
     * 
     * @param enableEmail
     *            enables/disables email alerts (or null)
     * @param enableSMS
     *            enables/disables sms alerts (or null)
     * @param enablePopup
     *            enables/disables popups (or null)
     * 
     * @see #setConfigProperty
     */
    private final void setConfigProperty(Boolean enableEmail, Boolean enableSMS, Boolean enablePopup) {
        String value = properties.getProperty(Configurator.REMOTE_ALARM_TYPES, "email,sms,popup");
        boolean email = value.indexOf("mail") != -1;
        boolean sms = value.indexOf("sms") != -1;
        boolean popup = value.indexOf("pop") != -1;
        if (enableEmail != null) {
            email = enableEmail.booleanValue();
        }
        if (enableSMS != null) {
            email = enableSMS.booleanValue();
        }
        if (enablePopup != null) {
            email = enablePopup.booleanValue();
        }
        if (!email && !sms && !popup) {
            email = true;
            sms = true;
            popup = true;
        }
        QuickWriter alarm = new QuickWriter(20);
        if (email) {
            alarm.write("email,");
        }
        if (sms) {
            alarm.write("sms,");
        }
        if (popup) {
            alarm.write("popup,");
        }
        alarm.setLength(alarm.length() - 1);
        setConfigProperty(Configurator.REMOTE_ALARM_TYPES, alarm.toString());
    }

    /**
     * Searches for the property with the specified key in the configuration.
     * The method returns the default value argument if the property is not
     * found (or empty).
     * 
     * @param key
     *            name of the config property
     * @param defaultValue
     *            a default value
     * 
     * @return the value in the config with the specified key value
     * 
     * @see #getConfigProperty
     */
    private final String getConfigProperty(String key, String defaultValue) {
        String value = properties.getProperty(key, defaultValue);
        if (value == null) {
            return defaultValue;
        } else {
            value = value.trim();
            if (value.length() == 0) {
                return defaultValue;
            }
        }
        return value;
    }

    /**
     * Searches for a boolean property with the specified key in the
     * configuration.
     * 
     * @param key
     *            name of the config property
     * @param defaultValue
     *            default boolean value
     * 
     * @return the value in the config with the specified key value
     * 
     * @see #getConfigProperty
     */
    private final boolean getConfigProperty(String key, boolean defaultValue) {
        String bool = properties.getProperty(key, Boolean.toString(defaultValue)).toLowerCase();
        return "true".equals(bool) || "on".equals(bool) || "1".equals(bool);
    }

    /**
     * Searches for a numeric (long) property with the specified key in the
     * configuration.
     * 
     * @param key
     *            name of the config property
     * @param defaultValue
     *            default long value
     * 
     * @return the value in the config with the specified key value
     * 
     * @see #getConfigProperty
     */
    private final long getConfigProperty(String key, long defaultValue) {
        String number = properties.getProperty(key, Long.toString(defaultValue));
        try {
            return StringUtils.stringToLong(number);
        } catch (Exception ignored) {
        }
        return defaultValue;
    }

    // --- PUBLIC PROPERTY GETTERS/SETTERS [SYNCHRONIZER] ---

    /**
     * Returns the value of the 'cache.timeout' property (= calendar timeout in
     * the local cache). The default value is '60000';
     * 
     * @return cache timeout in milliseconds (eg. 60000 = 1 minute)
     * 
     * @see #setCacheTimeout
     * @see #getConfigProperty
     */
    public final long getCacheTimeout() {
        return getConfigProperty(Configurator.CACHE_TIMEOUT, 60000L);
    }

    /**
     * Sets the value of the 'cache.timeout' property (= calendar timeout in the
     * local cache). Minimum (and default) value is 60000 milliseconds;
     * 
     * @param millis
     *            cache timeout in milliseconds (eg. 60000 = 1 minute)
     * 
     * @see #getCacheTimeout
     * @see #setConfigProperty
     */
    public final void setCacheTimeout(long millis) {

        // Verification (60000...n)
        if (millis < 60000L) {
            throw new IllegalArgumentException("cache.timeout < 1 min");
        }
        setConfigProperty(Configurator.CACHE_TIMEOUT, millis);
    }

    /**
     * Returns the value of the 'progress.enabled' property (= show animated
     * progress bar while synching). The default value is 'false'.
     * 
     * @return true or false (true = enabled)
     * 
     * @see #setProgressEnabled
     * @see #getConfigProperty
     */
    public final boolean getProgressEnabled() {
        return getConfigProperty(Configurator.PROGRESS_ENABLED, false);
    }

    /**
     * Sets the value of the 'progress.enabled' property (= show animated
     * progress bar while synching). The default value is 'false'.
     * 
     * @param enable
     *            true or false (true = enabled)
     * 
     * @see #getProgressEnabled
     * @see #setConfigProperty
     */
    public final void setProgressEnabled(boolean enable) {
        setConfigProperty(Configurator.PROGRESS_ENABLED, enable);
    }

    /**
     * Returns the value of the 'send.invitations' property (= Google Calendar
     * send an email to the attendees to invite them to attend). The default
     * value is 'false'.
     * 
     * @return true or false (true = enabled)
     * 
     * @see #setSendInvitations
     * @see #getConfigProperty
     */
    public final boolean getSendInvitations() {
        return getConfigProperty(Configurator.SEND_INVITATIONS, false);
    }

    /**
     * Sets the value of the 'send.invitations' property (= Google Calendar send
     * an email to the attendees to invite them to attend). The default value is
     * 'false'.
     * 
     * @param enable
     *            true or false (true = enabled)
     * 
     * @see #getSendInvitations
     * @see #setConfigProperty
     */
    public final void setSendInvitations(boolean enable) {
        setConfigProperty(Configurator.SEND_INVITATIONS, enable);
    }

    /**
     * Returns the value of the 'ical.backup.timeout' property (= backup file
     * timeout in the working directory). Default is 604800000 = one week, 0 =
     * disable backups.
     * 
     * @return backup timeout in milliseconds (eg. 604800000 = 1 week)
     * 
     * @see #setIcalBackupTimeout
     * @see #getConfigProperty
     */
    public final long getIcalBackupTimeout() {
        return getConfigProperty(Configurator.ICAL_BACKUP_TIMEOUT, 604800000L);
    }

    /**
     * Sets the value of the 'ical.backup.timeout' property (= backup file
     * timeout in the working directory). Default is 604800000 = one week, 0 =
     * disable backups.
     * 
     * @param millis
     *            backup timeout in milliseconds (eg. 604800000 = 1 week)
     * 
     * @see #getIcalBackupTimeout
     * @see #setConfigProperty
     */
    public final void setIcalBackupTimeout(long millis) {

        // Verification (0 or 86400000...n)
        if (millis < 86400000L && millis != 0) {
            throw new IllegalArgumentException("ical.backup.timeout < 1 day");
        }
        setConfigProperty(Configurator.ICAL_BACKUP_TIMEOUT, millis);
    }

    /**
     * Returns the value of the 'extended.sync.enabled' property (= enable to
     * sync alarms, categories, urls, priorities = full synchronization). The
     * default value is 'true'.
     * 
     * @return true or false (true = enabled)
     * 
     * @see #setExtendedSyncEnabled
     * @see #getConfigProperty
     */
    public final boolean getExtendedSyncEnabled() {
        return getConfigProperty(Configurator.EXTENDED_SYNC_ENABLED, true);
    }

    /**
     * Sets the value of the 'extended.sync.enabled' property (= enable to sync
     * alarms, categories, urls, priorities = full synchronization). The default
     * value is 'true'.
     * 
     * @param enable
     *            true or false (true = enabled)
     * 
     * @see #getExtendedSyncEnabled
     * @see #setConfigProperty
     */
    public final void setExtendedSyncEnabled(boolean enable) {
        setConfigProperty(Configurator.EXTENDED_SYNC_ENABLED, enable);
    }

    /**
     * Returns true if the Email alarm type is enabled. The default value is
     * 'true'.
     * 
     * @return true or false (true = enabled)
     * 
     * @see #setEmailAlarmsEnabled
     * @see #getConfigProperty
     */
    public final boolean getEmailAlarmsEnabled() {
        String value = getConfigProperty(Configurator.REMOTE_ALARM_TYPES, "email,sms,popup");
        return (value.indexOf("email") != -1);
    }

    /**
     * Enables/disables the Email alarm type. The default value is 'true'.
     * 
     * @param enable
     *            true or false (true = enabled)
     * 
     * @see #getEmailAlarmsEnabled
     * @see #setConfigProperty
     */
    public final void setEmailAlarmsEnabled(boolean enable) {
        setConfigProperty(Boolean.valueOf(enable), null, null);
    }

    /**
     * Returns true if the SMS alarm type is enabled. The default value is
     * 'true'.
     * 
     * @return true or false (true = enabled)
     * 
     * @see #setSMSAlarmsEnabled
     * @see #getConfigProperty
     */
    public final boolean getSMSAlarmsEnabled() {
        String value = getConfigProperty(Configurator.REMOTE_ALARM_TYPES, "email,sms,popup");
        return (value.indexOf("sms") != -1);
    }

    /**
     * Enables/disables the SMS alarm type. The default value is 'true'.
     * 
     * @param enable
     *            true or false (true = enabled)
     * 
     * @see #getSMSAlarmsEnabled
     * @see #setConfigProperty
     */
    public final void setSMSAlarmsEnabled(boolean enable) {
        setConfigProperty(null, Boolean.valueOf(enable), null);
    }

    /**
     * Returns true if the 'popup' alarm type is enabled. The default value is
     * 'true'.
     * 
     * @return true or false (true = enabled)
     * 
     * @see #setPopupAlarmsEnabled
     * @see #getConfigProperty
     */
    public final boolean getPopupAlarmsEnabled() {
        String value = getConfigProperty(Configurator.REMOTE_ALARM_TYPES, "email,sms,popup");
        return (value.indexOf("popup") != -1);
    }

    /**
     * Enables/disables the 'popup' alarm type. The default value is 'true'.
     * 
     * @param enable
     *            true or false (true = enabled)
     * 
     * @see #getPopupAlarmsEnabled
     * @see #setConfigProperty
     */
    public final void setPopupAlarmsEnabled(boolean enable) {
        setConfigProperty(null, null, Boolean.valueOf(enable));
    }

    // --- PUBLIC PROPERTY GETTERS/SETTERS [FEED CONVERTER] ---

    /**
     * Make sure the RSS/ATOM converter is available.
     * 
     * @throws UnsupportedOperationException
     *             feed converter unavailable (missing JARs)
     */
    private final void checkFeedConverter() throws UnsupportedOperationException {
        if (!getConfigProperty(Configurator.FEED_ENABLED, true)) {
            throw new UnsupportedOperationException("feed converter unavailable, check classpath");
        }
    }

    /**
     * Returns the value of the 'feed.cache.timeout' property (= timeout of the
     * RSS files in the memory cache). The default value is '3600000' (= 1
     * hour).
     * 
     * @return cache timeout in milliseconds (eg. 60000 = 1 minute)
     * 
     * @see #setFeedCacheTimeout
     * @see #getConfigProperty
     */
    public final long getFeedCacheTimeout() {
        return getConfigProperty(Configurator.FEED_CACHE_TIMEOUT, 3600000L);
    }

    /**
     * Sets the value of the 'feed.cache.timeout' property (= timeout of the RSS
     * files in the memory cache). The default value is '3600000' (= 1 hour).
     * 
     * @param millis
     *            cache timeout in milliseconds (min. 60000 = 1 minute)
     * 
     * @see #getFeedCacheTimeout
     * @see #setConfigProperty
     */
    public final void setFeedCacheTimeout(long millis) {

        // Make sure the RSS/ATOM converter is available
        checkFeedConverter();

        // Verification (60000...n)
        if (millis < 60000L) {
            throw new IllegalArgumentException("feed.cache.timeout < 1 min");
        }
        setConfigProperty(Configurator.FEED_CACHE_TIMEOUT, millis);
    }

    /**
     * Returns the value of the 'feed.event.length' property (= length of the
     * converted feed events in calendar). The default value is '2700000' (= 45
     * minutes).
     * 
     * @return event length in milliseconds (eg. 60000 = 1 minute)
     * 
     * @see #setFeedEventLength
     * @see #getConfigProperty
     */
    public final long getFeedEventLength() {
        return getConfigProperty(Configurator.FEED_EVENT_LENGTH, 2700000L);
    }

    /**
     * Sets the value of the 'feed.event.length' property (= length of the
     * converted feed events in calendar). The default value is '2700000' (= 45
     * minutes).
     * 
     * @param millis
     *            event length in milliseconds (min 60000 = 1 minute)
     * 
     * @see #getFeedEventLength
     * @see #setConfigProperty
     */
    public final void setFeedEventLength(long millis) {

        // Make sure the RSS/ATOM converter is available
        checkFeedConverter();

        // Verification (60000...n)
        if (millis < 60000L) {
            throw new IllegalArgumentException("feed.event.length < 1 min");
        }
        setConfigProperty(Configurator.FEED_EVENT_LENGTH, millis);
    }

    /**
     * Returns the value of the 'feed.duplication.filter' property (=
     * sensitivity of the RSS duplication filter, 50 = very sensitive, 100 =
     * disabled). The default value is '70'.
     * 
     * @return sensitivity (40 - 100)
     * 
     * @see #setFeedDuplicationFilter
     * @see #getConfigProperty
     */
    public final int getFeedDuplicationFilter() {
        return (int) getConfigProperty(Configurator.FEED_DUPLICATION_FILTER, 70);
    }

    /**
     * Sets the value of the 'feed.duplication.filter' property (= sensitivity
     * of the RSS duplication filter, 50 = very sensitive, 100 = disabled). The
     * default value is '70'.
     * 
     * @param percent
     *            sensitivity (40 - 100)
     * 
     * @see #getFeedDuplicationFilter
     * @see #setConfigProperty
     */
    public final void setFeedDuplicationFilter(int percent) {

        // Make sure the RSS/ATOM converter is available
        checkFeedConverter();

        // Verification (40...100)
        if (percent < 40) {
            throw new IllegalArgumentException("feed.duplication.filter < 40");
        }
        if (percent > 100) {
            throw new IllegalArgumentException("feed.duplication.filter > 100");
        }
        setConfigProperty(Configurator.FEED_DUPLICATION_FILTER, percent);
    }

}