ca.inverse.sogo.engine.source.SOGoUtilities.java Source code

Java tutorial

Introduction

Here is the source code for ca.inverse.sogo.engine.source.SOGoUtilities.java

Source

/*
 * Copyright (C) 2007-2010 Inverse inc. and Ludovic Marcotte
 * 
 * Author: Ludovic Marcotte <lmarcotte@inverse.ca>
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */
package ca.inverse.sogo.engine.source;

import java.io.ByteArrayInputStream;
import java.text.*;
import java.util.*;
import java.sql.*;

import com.funambol.common.pim.common.PropertyWithTimeZone;
import com.funambol.framework.logging.FunambolLogger;
import com.funambol.common.pim.common.Property;
import com.funambol.common.pim.contact.Contact;
import com.funambol.common.pim.icalendar.*;
import com.funambol.common.pim.model.*;
import com.funambol.common.pim.sif.SIFCParser;
import com.funambol.common.pim.sif.SIFCalendarParser;
import com.funambol.common.pim.utility.TimeUtils;
import com.funambol.common.pim.vcard.VcardParser;
import com.funambol.framework.core.DataStore;
import com.funambol.framework.engine.source.*;
import com.funambol.framework.server.*;
import com.funambol.common.pim.converter.*;
import com.funambol.common.pim.calendar.*;
import com.funambol.common.pim.calendar.Calendar;
import com.funambol.common.pim.converter.TaskToSIFT;
import com.funambol.common.pim.contact.*;
import com.funambol.framework.engine.SyncItem;
import org.json.simple.*;

@SuppressWarnings(value = { "unchecked" })
public class SOGoUtilities {

    // Constants
    static final int RX = 1;
    static final int TX = 2;
    // Classification codes mapping
    // iCal-vCard      ; Funambol   ; SOGo
    // PUBLIC      ; 0      ; 0
    // X-PERSONAL      ; 1      ; N/A
    // PRIVATE      ; 2      ; 1
    // CONFIDENTIAL      ; 3      ; 2
    public static final Short SENSITIVITY_PRIVATE = 2; // OlSensitivity.olPrivate
    public static final Short SENSITIVITY_CONFIDENTIAL = 3; // OlSensitivity.olConfidential
    public static final Short SOGO_SENSITIVITY_PRIVATE = 1;
    public static final Short SOGO_SENSITIVITY_CONFIDENTIAL = 2;

    /**
        
    Sample output of c_settings :
        
     Calendar = {
     DragHandleVertical = 122;
     FolderColors = {
         "flachapelle:Calendar/personal" = "#FF6666";
         "lmarcotte:Calendar/2633-49F5AB80-1-ECD55D0" = "#CC33FF";
         "lmarcotte:Calendar/personal" = "#99FF99";
     };
     FolderShowAlarms = {
         "lmarcotte:Calendar/personal" = YES;
     };
     FolderShowTasks = {
         "lmarcotte:Calendar/personal" = YES;
     };
     FolderSyncTags = {
         "lmarcotte:Calendar/2633-49F5AB80-1-ECD55D0" = "a bb oo";
         "lmarcotte:Calendar/personal" = toto;
     };
     InactiveFolders = (
     );
     SubscribedFolders = (
         "flachapelle:Calendar/personal"
     );
     View = weekview;
      };
          
      As an example, for :
          
      FolderSyncTags = {
         "lmarcotte:Calendar/2633-49F5AB80-1-ECD55D0" = "a bb oo";
      };
        
      we'll eventually check, in sogo_folder_info:
          
      c_folder_type = 'Appointment' AND
      c_path2 = 'lmarcotte' AND
      c_path ENDS WITH 'Calendar/2633-49F5AB80-1-ECD55D0'
          
     */
    public static HashMap getSyncTags(SOGoSyncSource source, int type, SyncContext context, String username,
            FunambolLogger log) {
        Vector<String> folders;
        HashMap h;

        h = new HashMap();

        // For now, we only support sync tags for calendars (events and tasks). So if we detect
        // a contact source, we return immediately.
        if (type == SOGoSyncSource.SOGO_CONTACT)
            return h;

        folders = new Vector<String>();

        try {
            ResultSet rs;
            Statement s;

            // We first fetch the user's time zone
            s = source.getDBConnection().createStatement();
            rs = s.executeQuery("SELECT c_settings FROM sogo_user_profile WHERE c_uid = '" + username + "'");

            if (rs.next()) {
                String data;

                data = rs.getString(1);

                // We've got no c_settings, return immediately
                if (data == null || data.length() == 0) {
                    rs.close();
                    s.close();
                    return h;
                }

                try {
                    JSONObject json, jsonCalendar;
                    log.info("About to parse: " + data);
                    json = (JSONObject) JSONValue.parse(data);

                    if (json != null) {
                        jsonCalendar = (JSONObject) json.get("Calendar");

                        if (jsonCalendar != null) {
                            String key, value;
                            Iterator it;

                            json = (JSONObject) jsonCalendar.get("FolderSyncTags");
                            if (json != null) {
                                it = json.keySet().iterator();
                                while (it != null && it.hasNext()) {
                                    key = it.next().toString();
                                    value = (String) json.get(key);
                                    h.put(value.toLowerCase(), key);
                                }
                            }

                            json = (JSONObject) jsonCalendar.get("FolderSynchronize");
                            if (json != null) {
                                it = json.keySet().iterator();
                                while (it != null && it.hasNext()) {
                                    folders.add(it.next().toString());
                                }
                            }
                        }
                    }
                } catch (Exception pe) {
                    log.error("Exception occured in getSyncTags(): " + pe.toString(), pe);
                }
            }

            // We cleanup what we have to sync, if necessary. We only keep
            // the keys that are actually in "folders". 
            h.values().retainAll(folders);
            rs.close();
            s.close();

        } catch (Exception e) {
            log.error("Exception occured in getSyncTags(): " + e.toString(), e);
        }

        return h;
    }

    /**
     * 
     * For now, we only return the tags associated to *calendars*.
     * 
     * @param source
     * @param sync_tags
     * @param log
     * @return
     */
    public static HashMap getSyncTagsLocation(SOGoSyncSource source, SyncContext context, HashMap sync_tags,
            FunambolLogger log) {
        HashMap h;

        h = new HashMap();

        try {
            String tag, c_path, c_path2, value;
            Vector<String> allTags;
            Iterator tags;
            ResultSet rs;
            Statement s;
            Set s1, s2;
            int i;

            // We first fetch the user's time zone
            s = source.getDBConnection().createStatement();

            s1 = sync_tags.keySet();
            tags = s1.iterator();

            while (tags.hasNext()) {
                tag = (String) tags.next();
                value = (String) sync_tags.get(tag);

                // The value we get is structured like: sogo1:Calendar/2230-4A609280-1-3C4950E0
                i = value.indexOf(':');
                c_path2 = value.substring(0, i);
                c_path = value.substring(i + 1);

                // We always skip the 'personal' folders (for the current user) and we only get the 'Appointment' collections for
                // now as it's not possible to set tags on additional address books from SOGo Web.
                if (c_path2.equalsIgnoreCase(context.getPrincipal().getUsername()))
                    rs = s.executeQuery(
                            "SELECT c_path, c_location FROM sogo_folder_info WHERE c_path4 != 'personal' AND c_folder_type = 'Appointment' AND c_path2 = '"
                                    + c_path2 + "'");
                else
                    rs = s.executeQuery(
                            "SELECT c_path, c_location FROM sogo_folder_info WHERE c_folder_type = 'Appointment' AND c_path2 = '"
                                    + c_path2 + "'");

                while (rs.next()) {
                    if (rs.getString(1).endsWith(c_path)) {
                        log.info("getSyncTagsLocation - caching key = " + tag + " for location: "
                                + rs.getString(2).substring(rs.getString(2).lastIndexOf('/') + 1));
                        h.put(tag, rs.getString(2).substring(rs.getString(2).lastIndexOf('/') + 1));
                    }
                }
                rs.close();
            }

            // We now clean all the crap in sync_tags - calendars might have been deleted 
            // and FolderSyncTags might be out of sync.
            allTags = new Vector(sync_tags.keySet());
            s2 = h.keySet();

            for (i = allTags.size() - 1; i >= 0; i--) {
                tag = (String) allTags.get(i);
                if (!s2.contains(tag))
                    s1.remove(tag);
            }

            s.close();

        } catch (Exception e) {
            log.error("Exception occured in getSyncTagsLocation(): " + e.toString(), e);
        }

        return h;
    }

    /**
     * This method is used to guess the best database driver
     * based on the supplied URI. The format is usually:
     * 
     * jdbc:postgresql:...
     * jdbc:oracle:thin:...
     * 
     * It then initialize the driver with the proper parameters and
     * establish a database connection, which is returned.
     */
    public static Connection initDatabaseDriver(SOGoSyncSource source, FunambolLogger log) {
        Connection con;
        Properties props;
        String s, uri;
        int a, b;

        props = new Properties();
        uri = source.getDatabaseURL();
        a = uri.indexOf(':');
        b = uri.indexOf(':', a + 1);
        s = uri.substring(a + 1, b);

        if (s.equalsIgnoreCase("oracle")) {
            s = "oracle.jdbc.OracleDriver";

            // For Oracle, we try to load big strings as CLOB. This works
            // with the ojdbc14.jar (Oracle 10g 10.1.0.2.0) driver.
            props.put("SetBigStringTryClob", "true");
        } else if (s.equalsIgnoreCase("mysql")) {
            s = "com.mysql.jdbc.Driver";
        } else {
            s = "org.postgresql.Driver";
        }

        props.put("user", source.getDatabaseUsername());
        props.put("password", source.getDatabasePassword());

        try {
            log.info("Loading the JDBC driver for URL: " + source.getDatabaseURL());
            Class.forName(s);

            con = DriverManager.getConnection(uri, props);

            // MySQL driver defaults to autocommit, so make sure it's turned off
            if (con.getAutoCommit()) {
                con.setAutoCommit(false);
            }

            return con;
        } catch (Exception e) {
            log.error("Couldn't find the driver! (" + s + ") or connect to the database: " + e.toString(), e);
        }

        return null;
    }

    /**
     * 
     * @param content
     * @param name
     * @param log
     * @return
     */
    public static VComponent getVComponentFromContent(String content, String name, FunambolLogger log) {
        VComponent c, component;
        ICalendarParser p;
        List l;
        int i;

        try {
            p = new ICalendarParser(new ByteArrayInputStream(content.getBytes()));
            component = p.ICalendar();
            l = component.getAllComponents();

            for (i = 0; i < l.size(); i++) {
                c = (VComponent) l.get(i);
                if (c.getVComponentName().equalsIgnoreCase(name)) {
                    return c;
                }
            }
        } catch (Exception e) {
            log.error("Exception occured in getVComponentFromContent(): " + e.toString(), e);
        }

        return null;
    }

    /**
     * 
     * @param item
     * @param source
     * @param type
     * @param log
     * @return
     */
    public static CalendarContent getCalendarContentFromSyncItem(String content, String name, SyncItem item,
            SOGoSyncSource source, int type, FunambolLogger log) {
        CalendarContent cc;

        cc = null;

        try {
            switch (type) {
            case SOGoSyncSource.VCALENDAR_VERSION_20:
            case SOGoSyncSource.VCALENDAR_VERSION_10:
                VComponent c;
                c = getVComponentFromContent(content, name, log);
                cc = new VCalendarContentConverter(null, source.getDeviceCharset(), true)
                        .vcc2cc((VCalendarContent) c, true);
                break;
            case SOGoSyncSource.X_S4J_SIFE:
            case SOGoSyncSource.X_S4J_SIFT:
                SIFCalendarParser p;
                p = new SIFCalendarParser(new ByteArrayInputStream(item.getContent()));
                cc = p.parse().getCalendarContent();
                break;
            }
        } catch (Exception e) {
            log.error(
                    "Exception occured in getCalendarContentFromSyncItem() - couldn't get Calendar from the syncItem: "
                            + e.toString(),
                    e);
            cc = null;
        }

        return cc;
    }

    /**
     * 
     * @param item
     * @param type
     * @param log
     * @return
     */
    public static Contact getContactFromSyncItem(SyncItem item, int type, FunambolLogger log) {
        Contact c;

        c = null;

        try {
            switch (type) {
            case SOGoSyncSource.VCARD_VERSION_30:
            case SOGoSyncSource.VCARD_VERSION_21: {
                VcardParser p;
                p = new VcardParser(new ByteArrayInputStream(item.getContent()), null, null);
                c = p.vCard();
            }
                break;

            case SOGoSyncSource.X_S4J_SIFC: {
                SIFCParser p;
                p = new SIFCParser(new ByteArrayInputStream(item.getContent()));
                c = p.parse();
            }
                break;
            }
        } catch (Exception e) {
            log.error("Exception occured in getContactFromSyncItem() - couldn't get Contact from the syncItem: "
                    + e.toString(), e);
            c = null;
        }

        return c;
    }

    /**
     * 
     * @param context
     * @return
     */
    public static boolean getDeviceUTC(SyncContext context) {
        Sync4jDevice device;

        device = context.getPrincipal().getDevice();

        if (device.getCapabilities().getDevInf().getUTC() != null
                && device.getCapabilities().getDevInf().getUTC().booleanValue()) {
            return true;
        }

        return false;
    }

    /**
     * 
     * @param c
     * @return
     */
    public static String getPreferredCity(Contact c) {
        String s;

        s = c.getBusinessDetail().getAddress().getCity().getPropertyValueAsString();

        if (s == null) {
            s = c.getPersonalDetail().getAddress().getCity().getPropertyValueAsString();
        }

        return s;
    }

    /**
     * 
     * @param item
     * @return
     */
    public static String getPreferredEmail(Contact c) {
        String s;
        List l;

        l = c.getBusinessDetail().getEmails();

        if (l.size() > 0) {
            s = ((Email) l.get(0)).getPropertyValueAsString();

            if (s != null && s.length() > 0)
                return s;
        }

        l = c.getPersonalDetail().getEmails();

        if (l.size() > 0) {
            s = ((Email) l.get(0)).getPropertyValueAsString();

            if (s != null && s.length() > 0)
                return s;
        }

        return null;
    }

    /**
     * 
     * @param context
     * @param syncSourceType
     * @param way
     * @return
     */
    public static int getPreferredItemType(SyncContext context, int syncSourceType, int way) {
        ArrayList<String> l_types, l_versions;
        ArrayList dataStores;
        DataStore ds;
        int i, type;

        // First, we get the supported types from our data store
        // and also their version numbers.
        try {
            dataStores = context.getPrincipal().getDevice().getCapabilities().getDevInf().getDataStores();
        } catch (Exception e) {
            // We haven't been able to get any device, capabilities or data stores. We simply
            // assume our connecting client only supports the SIF standard.
            switch (syncSourceType) {
            case SOGoSyncSource.SOGO_CONTACT:
                return SOGoSyncSource.X_S4J_SIFC;
            case SOGoSyncSource.SOGO_EVENT:
                return SOGoSyncSource.X_S4J_SIFE;
            default:
                return SOGoSyncSource.X_S4J_SIFT;
            }
        }

        l_versions = new ArrayList<String>();
        l_types = new ArrayList<String>();

        for (i = 0; i < dataStores.size(); i++) {
            ds = (DataStore) dataStores.get(i);

            if (way == RX) {
                l_types.add(ds.getRxPref().getCTType().trim());
                l_versions.add(ds.getRxPref().getVerCT().trim());
            } else {
                l_types.add(ds.getTxPref().getCTType().trim());
                l_versions.add(ds.getTxPref().getVerCT().trim());
            }
        }

        // Based on the current sync source type, we return the most appropriate
        // item's content type. This still is a guess as we have no way on linking
        // the Device's capabilities with a current SyncSource.
        switch (syncSourceType) {
        case SOGoSyncSource.SOGO_EVENT:
            if (l_types.contains("text/x-s4j-sife")) {
                type = SOGoSyncSource.X_S4J_SIFE;
            }
            // Let's assume we've got text/x-vcalendar
            else if (l_versions.contains("2.0")) {
                type = SOGoSyncSource.VCALENDAR_VERSION_20;
            } else {
                type = SOGoSyncSource.VCALENDAR_VERSION_10;
            }
            break;
        case SOGoSyncSource.SOGO_TODO:
            if (l_types.contains("text/x-s4j-sift")) {
                type = SOGoSyncSource.X_S4J_SIFT;
            }
            // Let's assume we've got text/x-vcalendar
            else if (l_versions.contains("2.0")) {
                type = SOGoSyncSource.VCALENDAR_VERSION_20;
            } else {
                type = SOGoSyncSource.VCALENDAR_VERSION_10;
            }
            break;
        case SOGoSyncSource.SOGO_CONTACT:
        default:
            if (l_types.contains("text/x-s4j-sifc")) {
                type = SOGoSyncSource.X_S4J_SIFC;
            }
            // Let's assume we've got text/x-vcard
            else if (l_versions.contains("3.0")) {
                type = SOGoSyncSource.VCARD_VERSION_30;
            } else {
                type = SOGoSyncSource.VCARD_VERSION_21;
            }
            break;
        }

        return type;
    }

    /**
     * 
     * @param c
     * @return
     */
    public static String getPreferredPhone(Contact c) {
        List l;

        l = c.getBusinessDetail().getPhones();

        if (l.size() > 0) {
            return ((Phone) l.get(0)).getPropertyValueAsString();
        }

        l = c.getPersonalDetail().getPhones();

        if (l.size() > 0) {
            return ((Phone) l.get(0)).getPropertyValueAsString();
        }

        return null;
    }

    /**
     * 
     * @param source
     * @param context
     * @param log
     * @return
     */
    public static TimeZone getUserTimeZone(SOGoSyncSource source, SyncContext context, FunambolLogger log) {
        String tz;

        tz = null;

        try {
            ResultSet rs;
            Statement s;

            // We first fetch the user's time zone
            s = source.getDBConnection().createStatement();
            rs = s.executeQuery("SELECT c_defaults FROM sogo_user_profile WHERE c_uid = '"
                    + context.getPrincipal().getUsername() + "'");

            if (rs.next()) {
                String data;
                Object o;

                data = rs.getString(1);
                o = null;

                try {
                    JSONObject json;

                    json = (JSONObject) JSONValue.parse(data);

                    try {
                        o = json.get("SOGoTimeZone");
                    } catch (Exception nfe) {
                        o = json.get("TimeZone");
                    }

                } catch (Exception pe) {
                    log.error("Exception occured in getUserTimeZone(): " + pe.toString(), pe);
                }

                if (o != null) {
                    tz = o.toString();
                }
            }

            rs.close();
            s.close();

        } catch (Exception e) {
            log.error("Exception occured in getUserTimeZone(): " + e.toString(), e);
        }

        if (tz == null) {
            log.info("No timezone defined in SOGo for user: " + context.getPrincipal().getUsername());
            return TimeZone.getTimeZone("GMT");
        }

        return TimeZone.getTimeZone(tz);
    }

    /**
     * Get time as seconds since the Epoc from a property.
     * The property must be in the form of yyyyMMddTHHmmssZ. If the input is not on this form
     * or an error occurs, <code>-1</code> will be returned. Any parse errors will be logged.
     *
     * @param prop The property to get the time from
     * @param defaultTZ Time zone to use if property has no time zone
     * @param log Application logger
     * @return The time, or <code>-1</code>
     */
    public static long getTimeFromProperty(PropertyWithTimeZone prop, TimeZone defaultTZ, FunambolLogger log) {
        if (prop == null) {
            log.debug("Property parameter is null");
            return -1;
        }

        String dateString = prop.getPropertyValueAsString();
        if (dateString == null || dateString.length() == 0) {
            log.debug("Property value is null or empty");
            return -1;
        }

        SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
        if (prop.getTimeZone() != null) {
            // Note! getTimeZone returns GMT if it can't parse the input
            formatter.setTimeZone(TimeZone.getTimeZone(prop.getTimeZone()));
        } else {
            formatter.setTimeZone(defaultTZ);
        }

        try {
            return formatter.parse(dateString).getTime() / 1000;
        } catch (java.text.ParseException e) {
            log.warn(String.format("Parse exception from input '%s'", dateString), e);
            return -1;
        }
    }

    /**
     * 
     * By default, we return 1 so the event is opaque.
     * 
     * We consider the event as non-opaque if we have:
     * 
     * TRANSP:1
     * 
     * or
     * 
     * TRANSP:TRANSPARENT
     * 
     * @param e
     * @return
     */
    public static int getTransparency(com.funambol.common.pim.calendar.Event e) {
        if (e.getTransp() != null) {
            String s;

            s = e.getTransp().getPropertyValueAsString();

            if (s != null && (s.equalsIgnoreCase("1") || s.equalsIgnoreCase("TRANSPARENT")))
                return 0;
        }

        return 1;
    }

    /**
     * 
     * Get the main event category as String.
     * 
     * We consider that the main category is the first appearing in the comma separated list 
     * 
     * @param e
     * @return The main category name as String or NULL if no category property can be read
     */
    public static String getMainCategory(com.funambol.common.pim.calendar.Event e, FunambolLogger log) {
        String s;
        if (e.getCategories() != null) {

            s = e.getCategories().getPropertyValueAsString();

            return s.split(",")[0];
        } else {
            return null;
        }
    }

    /**
     * 
     * @param item
     * @param log
     * @return
     */
    public static byte[] SIFCTovCard(byte[] item, SOGoSyncSource source, FunambolLogger log) {
        ContactToVcard conv;
        SIFCParser p;
        Contact c;

        log.info("About to convert (SIF-C -> vCard): " + new String(item));

        try {
            p = new SIFCParser(new ByteArrayInputStream(item));
            c = p.parse();

            conv = new ContactToVcard(null, source.getDeviceCharset());
            return conv.convert(c).getBytes();

        } catch (Exception e) {
            log.error("Exception occured in SIFCTovCard: " + e.toString(), e);
        }

        return null;
    }

    /**
     * This method is used to convert SIF events and tasks to vCalendar components.
     * It makes sure to use the proper converter in order to avoid cast exceptions
     * in Funambol.
     * 
     * @param item
     * @param log
     * @return
     */
    public static byte[] SIFTovCalendar(byte[] item, SOGoSyncSource source, FunambolLogger log) {
        com.funambol.common.pim.calendar.CalendarContent content;
        com.funambol.common.pim.calendar.Calendar c;
        VCalendarContentConverter conv;
        SIFCalendarParser p;
        StringBuffer sbuf;

        log.info("About to convert (SIF-{E,T} -> vCalendar): " + new String(item));

        try {
            p = new SIFCalendarParser(new ByteArrayInputStream(item));
            c = p.parse();

            if ((content = c.getTask()) != null) {
                SOGoSanitizer.sanitizeSIFTask((Task) content);
            } else {
                content = c.getEvent();
            }

            conv = new VCalendarContentConverter(null, source.getDeviceCharset(), true);

            // We wrap our Event / Task inside a Calendar object. This is required
            // since addvEventSyncItem() and other methods expect this.
            sbuf = new StringBuffer();
            sbuf.append("BEGIN:VCALENDAR\r\n");
            sbuf.append("VERSION:1.0\r\n");
            sbuf.append(conv.cc2vcc(content, true).toString());
            sbuf.append("END:VCALENDAR\r\n");

            return sbuf.toString().getBytes();

        } catch (Exception e) {
            log.error("Exception occured in SIFTovCalendar: " + e.toString(), e);
        }

        return null;
    }

    public static void secureCalendarContent(CalendarContent cc, String tag, int classification) {
        String s;

        switch (classification) {
        case SOGoACLManager.SOGoACLPrivate:
            s = "(Private)";
            break;
        case SOGoACLManager.SOGoACLConfidential:
            s = "(Confidential)";
            break;
        default:
            s = "(Public)";
        }
        cc.setSummary(new Property(tag + s));
        cc.setDescription(new Property(""));
        cc.setLocation(new Property(""));
        cc.setCategories(new Property(""));
        cc.setUid(new Property(""));
        cc.resetAttendees();
        cc.setDAlarm(new Property(""));
    }

    /**
     * This method is used to convert a vCalendar object from v2 to v1.
     * We might lose some information by doing so but we try our best 
     * to not to. Here are the required transformations.
     * 
     * 1- removal of all VTIMEZONE information + UTC offset adjustments
     * 2- start/end (or due) date adjustments if the connecting device
     *    doesn't support UTC
     * 3- if we get TZID in DTSTART/DTEND, convert DTSTART/DTEND to UTC
     *    if our devices supports it, otherwise, leave it as is
     *    
     *    ---------------------------------------------------------------------------
     *    Devices supports UTC   | Input            | TZ to use | Output
     *    ----------------------|-------------------|-----------|--------------------
     *    YES               | UTC            | none      | UTC
     *    YES               | non-UTC         | pref. TZ   | UTC
     *    YES               | non-UTC + TZID   | TZID      | UTC
     *    NO               | UTC            | pref. TZ   | non-UTC
     *    NO               | non-UTC         | none      | non-UTC
     *    NO               | non-UTC + TZID   | none      | non-UTC
     * 
     * @param bytes
     * @return
     */
    public static byte[] vCalendarV2toV1(byte[] bytes, String tag, String key, boolean secure, int classification,
            SOGoSyncSource source, SyncContext context, FunambolLogger log) {
        String s;

        log.info("About to convert vCalendar (from v2 to v1): " + new String(bytes));
        s = "";

        try {
            VCalendarConverter converter;
            VComponentWriter writer;
            VCalendar v2, calendar;
            CalendarContent cc;
            ICalendarParser p;
            Calendar cal;

            // We need to proceed with the following steps
            // iCalendar -> VCalendar object -> Calendar object -> VCalendar v1.0 object -> string
            p = new ICalendarParser(
                    new ByteArrayInputStream(SOGoSanitizer.sanitizevCalendarInput(bytes, key, log).getBytes()));
            v2 = p.ICalendar();

            converter = new VCalendarConverter(getUserTimeZone(source, context, log), source.getDeviceCharset(),
                    false);
            cal = converter.vcalendar2calendar(v2);

            // HACK: Funambol (at least, v6.5-7.1) crashes in VCalendarContentConverter: cc2vcc
            //       since it is trying to get this property's value while it might be null.
            SOGoSanitizer.sanitizeFunambolCalendar(cal);
            cc = cal.getCalendarContent();

            if (tag != null) {
                String summary;

                tag = "[" + tag + "] ";
                summary = cc.getSummary().getPropertyValueAsString();
                if (!summary.startsWith(tag)) {
                    summary = tag + summary;
                    cc.setSummary(new Property(summary));
                }

                if (secure) {
                    secureCalendarContent(cc, tag, classification);
                }
            }

            // The Funambol client for BlackBerry devices is a real piece of crap.
            // It does NOT honor the X-FUNAMBOL-ALLDAY tag so we must do some magic
            // here to make it believe it's an all-day event. Otherwise, the all-day
            // event will span two days on the BlackBerry device.
            if (isBlackBerry(context) && cc.isAllDay()) {
                SimpleDateFormat formatter;
                java.util.Date d;
                String ss;

                // We can either parse 20120828 or 2012-08-28
                ss = cc.getDtStart().getPropertyValueAsString();
                d = null;

                try {
                    formatter = new SimpleDateFormat("yyyy-MM-dd");
                    d = formatter.parse(ss);
                } catch (Exception pe) {
                }

                if (d == null) {
                    try {
                        formatter = new SimpleDateFormat("yyyyMMdd");
                        d = formatter.parse(ss);
                    } catch (Exception pe) {
                    }
                }

                if (d != null) {
                    formatter = new SimpleDateFormat("yyyyMMdd");
                    ss = formatter.format(d) + "T000000";
                    cc.getDtStart().setPropertyValue(ss);

                    ss = formatter.format(d) + "T235900";
                    cc.getDtEnd().setPropertyValue(ss);

                }
            }

            // The boolean parameter triggers v1 vs. v2 conversion (v1 == true)
            calendar = converter.calendar2vcalendar(cal, true);
            copyCustomProperties(v2.getVCalendarContent(), calendar.getVCalendarContent());

            // Funambol loses alarm settings when converting to a Calendar object, see
            // http://forge.ow2.org/tracker/index.php?func=detail&aid=314860&group_id=96&atid=100096
            // Try to convert any valarm from v2 to aalarm.
            // This is implementation will probably fix 95% of the cases, but doesn't cover
            // multiple alarms or alarm times set relative to the end of the event
            // Also note that at least Symbian devices does not seem to support relative alarms
            VAlarm valarm = (VAlarm) v2.getVCalendarContent().getComponent("VALARM");
            log.info("VALARM is: " + valarm);
            if (valarm != null) {
                com.funambol.common.pim.model.Property trigger;

                // TRIGGER;VALUE=DURATION:-PT15M
                trigger = valarm.getProperty("TRIGGER");

                log.info("TRIGGER: " + trigger.getValue()); // -PT15

                if (trigger.getParameter("VALUE") != null) {
                    String duration;

                    duration = trigger.getParameter("VALUE").value;

                    if (duration.equalsIgnoreCase("DURATION")) {
                        String value;

                        boolean negate;
                        int len, i, v;
                        char c;

                        value = trigger.getValue(); //  -PT15
                        log.info("VALUE to parse: " + value);
                        len = value.length();
                        negate = false;

                        // v is always in seconds
                        v = 0;

                        // We parse :
                        //
                        // dur-value  = (["+"] / "-") "P" (dur-date / dur-time / dur-week)
                        //
                        // dur-date   = dur-day [dur-time]
                        // dur-time   = "T" (dur-hour / dur-minute / dur-second)
                        // dur-week   = 1*DIGIT "W"
                        // dur-hour   = 1*DIGIT "H" [dur-minute]
                        // dur-minute = 1*DIGIT "M" [dur-second]
                        // dur-second = 1*DIGIT "S"
                        // dur-day    = 1*DIGIT "D"
                        //
                        for (i = 0; i < len; i++) {
                            c = value.charAt(i);

                            if (c == 'P' || c == 'p' || c == 'T' || c == 't') {
                                log.info("Skipping " + c);
                                continue;
                            }

                            if (c == '-') {
                                negate = true;
                                log.info("Negating...");
                                continue;
                            }

                            if (Character.isDigit(c)) {
                                int j, x;

                                log.info("Digit found at " + i);

                                for (j = i; j < len; j++) {
                                    c = value.charAt(j);
                                    if (!Character.isDigit(c))
                                        break;
                                }

                                log.info("End digit found at " + i);

                                x = Integer.parseInt(value.substring(i, j));
                                log.info("x = " + x);

                                // Char at j is either W, H, M, S or D
                                switch (value.charAt(j)) {
                                case 'W':
                                    v += x * (7 * 24 * 3600);
                                    break;
                                case 'H':
                                    v += x * 3600;
                                    break;
                                case 'M':
                                    v += x * 60;
                                    break;
                                case 'S':
                                    v += x;
                                    break;
                                case 'D':
                                default:
                                    v += x * (24 * 3600);
                                    break;

                                }
                                log.info("v1 = " + v);

                                i = j + 1;
                            }
                        } // for (...)

                        log.info("v2 = " + v);

                        if (negate)
                            v = -v;

                        log.info("v3 = " + v);

                        // Let's add this to our start time.. we'll support end time later
                        if (v != 0) {
                            SimpleDateFormat dateFormatter;
                            java.util.Date d;

                            log.info("DTSTART: " + cc.getDtStart().getPropertyValue());
                            log.info("v = " + v);

                            try {
                                dateFormatter = new SimpleDateFormat(TimeUtils.PATTERN_UTC);
                                d = dateFormatter.parse(cc.getDtStart().getPropertyValueAsString());
                                d.setTime(d.getTime() + v * 1000);

                                calendar.getVCalendarContent().addProperty("AALARM", dateFormatter.format(d)); // DURATION
                            } catch (Exception e) {
                                log.error("Exception occured in vCalendarV2toV1(): " + e.toString(), e);
                            }
                        }
                    }

                }
            }

            // Write result
            writer = new VComponentWriter(VComponentWriter.NO_FOLDING);
            s = writer.toString(calendar);
            log.info(s);
        } catch (Exception e) {
            log.error("Exception occured in vCalendarV2toV1(): " + e.toString(), e);
            log.info("===== item content (" + key + ") =====");
            log.info(new String(bytes));
            log.info("======================================");
        }

        return s.getBytes();
    }

    /**
     * Copy vendor specific properties from a calendar object to another.
     * Funambol doesn't preserve these properties when converting to a Calendar object,
     * this method is intended to be used after a Calendar round-trip.
     *
     * @param from The calendar object to copy from
     * @param to   The calendar object to copy to
     */
    public static void copyCustomProperties(VCalendarContent from, VCalendarContent to) {
        for (Object o : from.getAllProperties()) {
            com.funambol.common.pim.model.Property p = (com.funambol.common.pim.model.Property) o;

            if (p.getName().startsWith("X-") && !p.getName().equals("X-FUNAMBOL-ALLDAY")) {
                to.addProperty(p);
            }
        }
    }

    /**
     * 
     * @param cc
     * @return
     */
    public static int getClassification(CalendarContent cc) {
        int classification, ac;

        classification = 0;
        if (cc.getAccessClass() != null) {
            ac = Integer.parseInt(cc.getAccessClass().getPropertyValueAsString());
            if (ac == SENSITIVITY_PRIVATE)
                classification = SOGO_SENSITIVITY_PRIVATE;
            if (ac == SENSITIVITY_CONFIDENTIAL)
                classification = SOGO_SENSITIVITY_CONFIDENTIAL;
        }
        return classification;
    }

    /**
     * 
     * @param conn
     * @param table
     * @param c_name
     */
    public static void updateContentVersion(Connection conn, String table, String c_name) throws Exception {
        ResultSet rs;
        Statement s;
        int v;

        s = conn.createStatement();
        rs = s.executeQuery("SELECT c_version FROM " + table + " WHERE c_name = '" + c_name + "'");
        v = 1;

        if (rs.next()) {
            v = rs.getInt(1);
            v++;
        }

        rs.close();

        s.executeUpdate("UPDATE " + table + " SET c_version = " + v + " WHERE c_name = '" + c_name + "'");
        s.close();
    }

    /**
     * 
     * @param item
     * @param log
     * @return
     */
    public static byte[] vCardToSIFC(byte[] item, SOGoSyncSource source, FunambolLogger log) {
        ContactToSIFC conv;
        VcardParser p;
        Contact c;

        try {
            p = new VcardParser(new ByteArrayInputStream(item), null, null);
            c = p.vCard();

            conv = new ContactToSIFC(null, source.getDeviceCharset());

            return conv.convert(c).getBytes();
        } catch (Exception e) {
            log.error("Exception occured in vCardToSIFC().", e);
        }

        return null;
    }

    public static boolean isIPhone(SyncContext context) {
        Sync4jDevice device;
        boolean b;

        b = false;

        device = context.getPrincipal().getDevice();

        if (device.getCapabilities().getDevInf().getMod() != null
                && device.getCapabilities().getDevInf().getMod().equalsIgnoreCase("iPhone")) {
            b = true;
        }

        return b;
    }

    public static boolean isBlackBerry(SyncContext context) {
        Sync4jDevice device;
        boolean b;

        b = false;

        device = context.getPrincipal().getDevice();

        if (device.getCapabilities().getDevInf().getMan() != null
                && device.getCapabilities().getDevInf().getMan().equalsIgnoreCase("Research In Motion")) {
            b = true;
        }

        return b;
    }

    /**
     * This method is used to convert a vCard object from v3 to v2.1.
     * 
     * We use Funambol API to do this at its producer only supports v2.1 :) 
     * 
     * @param bytes
     * @return
     */
    public static byte[] vCardV3toV21(byte[] bytes, SOGoSyncSource source, SyncContext context,
            FunambolLogger log) {
        try {
            ContactToVcard conv;
            VcardParser p;
            Contact c;
            String s;
            int a, b;

            log.info("About to convert vCard (from v3 to v2.1): " + new String(bytes) + "\niPhone? "
                    + isIPhone(context));
            s = SOGoSanitizer.sanitizevCardInput(bytes, context, log);
            s = s.replace("VERSION:3.0", "VERSION:2.1");

            // We replace properties with extra parameters
            if (!isIPhone(context))
                s = s.replace("EMAIL;TYPE=INTERNET,WORK", "EMAIL;INTERNET"); // BB or others
            else
                s = s.replace("EMAIL;TYPE=INTERNET,WORK", "EMAIL;INTERNET;WORK"); // iPhone OS
            s = s.replace("EMAIL;TYPE=INTERNET,HOME", "EMAIL;INTERNET;HOME");
            s = s.replace("TEL;TYPE=VOICE,WORK", "TEL;VOICE;WORK");
            s = s.replace("TEL;TYPE=VOICE,HOME", "TEL;VOICE;HOME");
            s = s.replace("TEL;TYPE=WORK,FAX", "TEL;FAX;WORK");

            // We replace properties with no extra parameters
            if (!isIPhone(context))
                s = s.replace("EMAIL;TYPE=WORK", "EMAIL;INTERNET"); // BB or others
            else
                s = s.replace("EMAIL;TYPE=WORK", "EMAIL;INTERNET;WORK"); // iPhone OS

            s = s.replace("EMAIL;TYPE=HOME", "EMAIL;INTERNET;HOME");
            s = s.replace("TEL;TYPE=WORK", "TEL;VOICE;WORK");
            s = s.replace("TEL;TYPE=HOME", "TEL;VOICE;HOME");
            s = s.replace("TEL;TYPE=CELL", "TEL;CELL");
            s = s.replace("TEL;TYPE=FAX", "TEL;FAX;WORK");
            s = s.replace("TEL;TYPE=PAGER", "TEL;PAGER");
            s = s.replace("ADR;TYPE=WORK", "ADR;WORK");
            s = s.replace("ADR;TYPE=HOME", "ADR;HOME");
            s = s.replace("URL;TYPE=WORK", "URL;WORK");
            s = s.replace("URL;TYPE=HOME", "URL;HOME");

            // We have to take care of the NOTE field which can have \r\n fields in it
            // iPhone sends this:   NOTE;ENCODING=QUOTED-PRINTABLE;CHARSET=UTF-8:Line 1=0Aline 2=0Aline 3
            // BB sends this:       NOTE;ENCODING=QUOTED-PRINTABLE;CHARSET=UTF-8:Line 1=0Aline 2=0Aline 3
            a = s.indexOf("\r\nNOTE:");

            if (a < 0)
                a = s.indexOf("\r\nNOTE;");

            if (a > 0) {
                b = s.indexOf(':', a);
                if (b > 0) {
                    boolean encoded;
                    int start, end;

                    // We intelligently search the end of the NOTE field. It could be folded, ie., end with \r\n
                    // but the line following stats with a  space or tab
                    start = b;

                    while (true) {
                        end = s.indexOf("\r\n", start);
                        if (end < 0)
                            break;

                        log.info("char end+2: |" + s.charAt(end + 2) + "|");
                        if (s.charAt(end + 2) != ' ' && s.charAt(end + 2) != '\t')
                            break;

                        start = start + (end - start + 2);
                    }

                    if (end > 0) {
                        String value;

                        value = s.substring(b + 1, end);

                        encoded = (value.indexOf("\\r\\n") > 0);

                        // We must replace \r\n with =0A
                        if (encoded) {
                            log.info("Initial value: " + value);

                            value = value.replace("\\r\\n", "=0A");
                            value = value.replace("\r\n", "");
                            log.info("Converted value: " + value);

                            s = s.substring(0, a) + "\r\nNOTE;ENCODING=QUOTED-PRINTABLE;CHARSET=UTF-8:" + value
                                    + s.substring(end);
                        }
                    }
                }

            }

            log.info("Downgraded vCard: " + s);

            // iPhone supports UTF-8
            if (isIPhone(context))
                return s.getBytes();

            // We must encode everything in QP
            p = new VcardParser(new ByteArrayInputStream(s.getBytes()), null, null);
            c = p.vCard();
            conv = new ContactToVcard(TimeZone.getTimeZone("GMT"), source.getDeviceCharset());

            s = conv.convert(c);
            log.info("Encoded vCard: " + s);

            return s.getBytes();

        } catch (Exception e) {
            log.error("Exception occured in vCardV3toV21(): " + e.toString(), e);
        }

        return null;
    }

    /**
     * 
     * @param item
     * @param log
     * @return
     */
    public static byte[] vEventToSIFE(byte[] item, String tag, String key, boolean secure, int classification,
            SOGoSyncSource source, SyncContext context, FunambolLogger log) {
        VCalendarContentConverter conv;
        VComponent c;
        ICalendarParser p;
        List l;
        int i;

        try {
            p = new ICalendarParser(new ByteArrayInputStream(item));
            c = p.ICalendar();

            conv = new VCalendarContentConverter(SOGoUtilities.getUserTimeZone(source, context, log),
                    source.getDeviceCharset(), true);

            l = c.getAllComponents();

            for (i = 0; i < l.size(); i++) {

                if (l.get(i) instanceof VCalendarContent) {
                    com.funambol.common.pim.calendar.Calendar cal;
                    VCalendarContent vcc;
                    CalendarToSIFE ctse;

                    vcc = (VCalendarContent) l.get(i);
                    cal = new com.funambol.common.pim.calendar.Calendar(conv.vcc2cc(vcc, false));

                    if (tag != null) {
                        com.funambol.common.pim.calendar.Event e;
                        String s;

                        e = cal.getEvent();
                        tag = "[" + tag + "] ";
                        s = tag + e.getSummary().getPropertyValueAsString();
                        e.setSummary(new Property(s));

                        if (secure) {
                            secureCalendarContent(e, tag, classification);
                        }
                    }

                    ctse = new CalendarToSIFE(null, source.getDeviceCharset());
                    return ctse.convert(cal).getBytes();
                }
            }

        } catch (Exception e) {
            log.error("Exception occured in vEventToSIFE().", e);
            log.info("===== item content (" + key + ") =====");
            log.info(new String(item));
            log.info("======================================");
        }

        return null;
    }

    /**
     * 
     * @param item
     * @param log
     * @return
     */
    public static byte[] vTodoToSIFT(byte[] item, String tag, String key, boolean secure, int classification,
            SOGoSyncSource source, SyncContext context, FunambolLogger log) {
        VCalendarContentConverter conv;
        ICalendarParser p;
        VComponent c;
        List l;
        int i;

        try {
            p = new ICalendarParser(new ByteArrayInputStream(item));
            c = p.ICalendar();
            conv = new VCalendarContentConverter(TimeZone.getTimeZone("GMT"), source.getDeviceCharset(), true);

            l = c.getAllComponents();
            for (i = 0; i < l.size(); i++) {

                if (l.get(i) instanceof VCalendarContent) {
                    TaskToSIFT tts;
                    Task t;

                    t = (Task) conv.vcc2cc((VCalendarContent) l.get(i), true);

                    if (tag != null) {
                        String s;

                        tag = "[" + tag + "] ";
                        s = tag + t.getSummary().getPropertyValueAsString();
                        t.setSummary(new Property(s));

                        if (secure) {
                            secureCalendarContent(t, tag, classification);
                        }
                    }

                    tts = new TaskToSIFT(null, source.getDeviceCharset());
                    SOGoSanitizer.sanitizeSIFTask(t);

                    return tts.convert(t).getBytes();
                }
            }
        } catch (Exception e) {
            log.error("Exception occured in vTodoToSIFT().", e);
            log.info("===== item content (" + key + ") =====");
            log.info(new String(item));
            log.info("======================================");
        }

        return null;
    }

    /**
     * 
     * @param content
     * @param tag
     * @param source
     * @param context
     * @param log
     * @return
     */
    public static String removeTagFromContent(String content, String tag, SOGoSyncSource source,
            SyncContext context, FunambolLogger log) {
        try {
            VCalendarConverter converter;
            VCalendar calendar;
            CalendarContent cc;
            ICalendarParser p;
            String summary;
            Calendar cal;
            VComponentWriter writer;

            p = new ICalendarParser(new ByteArrayInputStream(content.getBytes()));
            calendar = p.ICalendar();

            converter = new VCalendarConverter(getUserTimeZone(source, context, log), source.getDeviceCharset(),
                    true);
            cal = converter.vcalendar2calendar(calendar);

            cc = cal.getCalendarContent();
            summary = cc.getSummary().getPropertyValueAsString();

            tag = ("[" + tag + "]").toLowerCase();
            if (summary.toLowerCase().startsWith(tag)) {
                summary = summary.substring(tag.length() + 1);
            }

            cc.setSummary(new Property(summary));

            calendar = converter.calendar2vcalendar(cal, true);

            writer = new VComponentWriter(VComponentWriter.NO_FOLDING);
            return writer.toString(calendar);
        } catch (Exception e) {
            log.error("Exception occured in removeTagFromContent: " + e.toString(), e);
        }

        return content;
    }
}