/* * Funambol is a mobile platform developed by Funambol, Inc. * Copyright (C) 2003 - 2007 Funambol, Inc. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU Affero General Public License version 3 as published by * the Free Software Foundation with the addition of the following permission * added to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED * WORK IN WHICH THE COPYRIGHT IS OWNED BY FUNAMBOL, FUNAMBOL DISCLAIMS THE * WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS. * * 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 Affero General Public License * along with this program; if not, see http://www.gnu.org/licenses or write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301 USA. * * You can contact Funambol, Inc. headquarters at 643 Bair Island Road, Suite * 305, Redwood City, CA 94063, USA, or at email address info@funambol.com. * * The interactive user interfaces in modified source and object code versions * of this program must display Appropriate Legal Notices, as required under * Section 5 of the GNU Affero General Public License version 3. * * In accordance with Section 7(b) of the GNU Affero General Public License * version 3, these Appropriate Legal Notices must retain the display of the * "Powered by Funambol" logo. If the display of the logo is not reasonably * feasible for technical reasons, the Appropriate Legal Notices must display * the words "Powered by Funambol". */ package com.funambol.common.pim.model.converter; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.HashMap; import java.util.Hashtable; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.TimeZone; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.funambol.util.QuotedPrintable; import com.funambol.common.pim.model.calendar.Calendar; import com.funambol.common.pim.model.calendar.CalendarContent; import com.funambol.common.pim.model.common.PropertyWithTimeZone; import com.funambol.common.pim.model.common.Property; import com.funambol.common.pim.model.model.Parameter; import com.funambol.common.pim.model.model.VCalendar; import com.funambol.common.pim.model.model.VCalendarContent; import com.funambol.common.pim.model.model.VComponent; import com.funambol.common.pim.model.model.VTimezone; import com.funambol.common.pim.model.utility.TimeUtils; /** * This object is a converter from a Calendar object model to a VCalendar string * * @see Converter * @version $Id: VCalendarConverter.java,v 1.13 2008-09-03 09:51:04 mauro Exp $ */ public class VCalendarConverter extends BaseConverter { final private String DATE_OR_DATE_TIME_REGEX = "([1-2][0-9]{3}(\\-)?[0-1][0-9](\\-)?[0-3][0-9])(T[0-2][0-9][0-5][0-9][0-5][0-9](Z)?)?"; final private String DATE_TIME_REGEX = "[1-2][0-9]{3}[0-1][0-9][0-3][0-9]T[0-2][0-9][0-5][0-9][0-5][0-9](Z)?"; // Policies for conversion of date/time properties: final private int FLOATING_POLICY = 0; final private int UTC_POLICY = 1; final private int PROPERTY_TZ_POLICY = 2; final private int CLIENT_TZ_POLICY = 3; // ------------------------------------------------------------- Constructor /** * This constructor is deprecated because to handle the date is need to know * timezone but also if the dates must be converted in local time. */ @Deprecated public VCalendarConverter(TimeZone timezone, String charset) { super(timezone, charset); } /** * * @param timezone the timezone to use in the conversion * @param charset the charset * @param forceClientLocalTime true if the date must be converted in the * client local time, false otherwise. */ public VCalendarConverter(TimeZone timezone, String charset, boolean forceClientLocalTime) { super(timezone, charset, forceClientLocalTime); } // ---------------------------------------------------------- Public Methods public String convert(Object obj) throws ConverterException { return null; } /** * Performs the VCalendar-to-Calendar conversion, finding out automatically * which type of calendar (Event or Todo) it is. * * @param vcal the VCalendar object to be converted * @return a Calendar containing the converted representation of this * VCalendar * @throws com.funambol.common.pim.converter.ConverterException */ public Calendar vcalendar2calendar(VCalendar vcal) throws ConverterException { VCalendarContentConverter vccc = new VCalendarContentConverter(timezone, charset, false); Calendar cal= new Calendar(); setCommonProperties(cal, vcal); boolean xv = false; if (vcal.getProperty("VERSION") != null && vcal.getProperty("VERSION").getValue() != null && "1.0".equals(vcal.getProperty("VERSION").getValue())) { xv = true; } VCalendarContent vcc = vcal.getVCalendarContent(); // Creates the list of available timezones (often, just one) Map<String, TimeZoneHelper> timeZones = new Hashtable<String, TimeZoneHelper>(3); if (xv) { com.funambol.common.pim.model.model.Property tz = vcal.getProperty("TZ"); List<com.funambol.common.pim.model.model.Property> daylightList = vcal.getProperties("DAYLIGHT"); try { TimeZoneHelper vctz = new CachedTimeZoneHelper(tz , daylightList); timeZones.put(vctz.getName(), vctz); } catch (Exception e) { timeZones.clear(); } } else { long[] interval = vccc.extractInterval(vcal.getVCalendarContent()); List<VComponent> vTimezones = vcal.getComponents(VTimezone.COMPONENT_NAME); for (VComponent vTimezone : vTimezones) { try { TimeZoneHelper vctz = new CachedTimeZoneHelper((VTimezone) vTimezone, interval[0] , interval[1] ); timeZones.put(vctz.getName(), vctz); } catch (Exception e) { // Skips this one } } } // Fill the three time-zone "slots" TimeZone dtStartTimeZone, dtEndTimeZone, reminderTimeZone; if (timeZones.isEmpty()) { dtStartTimeZone = dtEndTimeZone = reminderTimeZone = timezone; } else if (xv) { String allTimeZones = timeZones.get("").toID(timezone); if (allTimeZones == null) { dtStartTimeZone = dtEndTimeZone = reminderTimeZone = timezone; } else { dtStartTimeZone = dtEndTimeZone = reminderTimeZone = TimeZone.getTimeZone(allTimeZones); } } else { // only iCalendar (vCalendar 2.0) items can reach this point List<com.funambol.common.pim.model.model.Property> dtStart = new ArrayList<com.funambol.common.pim.model.model.Property>(1); dtStart.add(vcc.getProperty("DTSTART")); dtStartTimeZone = matchTimeZone(dtStart, timeZones); List<com.funambol.common.pim.model.model.Property> dtEnd = new ArrayList<com.funambol.common.pim.model.model.Property>(2); dtEnd.add(vcc.getProperty("DTEND")); dtEnd.add(vcc.getProperty("DUE")); dtEndTimeZone = matchTimeZone(dtEnd, timeZones); /* List<com.funambol.common.pim.model.Property> reminders = new ArrayList<com.funambol.common.pim.model.Property>(3); // @todo fill reminders... reminderTimeZone = matchTimeZone(reminders, timeZones); */ reminderTimeZone = null; // Provisional "solution" } CalendarContent cc = vccc.vcc2cc(vcc , xv , dtStartTimeZone , dtEndTimeZone , reminderTimeZone); cal.setCalendarContent(cc); return cal; } /** * Performs the Calendar-to-VCalendar conversion. * * @param cal the Calendar object to be converted * @param xv true if the text/x-vcalendar format must be used while * generating some properties of the VCalendar output object * @return a VCalendar containing the converted representation of this * Calendar * @throws com.funambol.common.pim.converter.ConverterException */ public VCalendar calendar2vcalendar(Calendar cal, boolean xv) throws ConverterException { VCalendar vcal= new VCalendar(); CalendarContent cc = cal.getCalendarContent(); setCommonProperties(vcal, cal, xv); String version = (xv ? "1.0" : "2.0"); vcal.setProperty(new com.funambol.common.pim.model.model.Property( "VERSION", false, new ArrayList(), version)); VCalendarContentConverter vccc = new VCalendarContentConverter(timezone, charset, forceClientLocalTime); VCalendarContent vcc = vccc.cc2vcc(cc, xv); if (xv && !forceClientLocalTime) { String id = cc.getDtStart().getTimeZone(); if ((id != null) && (id.length() != 0)) { long[] interval = cc.extractInterval(); TimeZoneHelper tz = new CachedTimeZoneHelper(id , interval[0], interval[1]); for (com.funambol.common.pim.model.model.Property xvCalendarProperty : tz.getXVCalendarProperties()){ vcal.addProperty(xvCalendarProperty); } } } else if (!xv) { List<String> timeZoneIDs = new ArrayList<String>(3); addIfNeeded(cc.getDtStart() , timeZoneIDs); addIfNeeded(cc.getDtEnd() , timeZoneIDs); addIfNeeded(cc.getReminder(), timeZoneIDs); if (forceClientLocalTime) { if (hasNoTimeZone(cc.getDtStart()) || hasNoTimeZone(cc.getDtEnd()) || hasNoTimeZone(cc.getReminder()) ) { // In this case, the device TZ might need to be added to the // list String clientTimeZoneID = timezone.getID(); boolean addClientTimeZoneID = true; for (String existingID : timeZoneIDs) { if (existingID.equals(clientTimeZoneID)) { addClientTimeZoneID = false; break; } } if (addClientTimeZoneID) { timeZoneIDs.add(timezone.getID()); } } } if (!timeZoneIDs.isEmpty()) { long[] interval = cc.extractInterval(); for (String id : timeZoneIDs) { TimeZoneHelper tz = new CachedTimeZoneHelper(id , interval[0], interval[1]); VTimezone vtz = tz.getVTimezone(); vcal.addComponent(vtz); } } } vcal.addComponent(vcc); return vcal; } //-------------------------------------------------------- Protected Methods /** * Creates an HashMap with the X-Param set extracted from a * com.funambol.common.pim.model.Property object. The encoding and charset * parameters are ignored. * * @param property the Property object (containing the X-Param ArrayList) * @return a list of X-Param's */ protected Map<String, String> getParameters( com.funambol.common.pim.model.model.Property property) { Map<String, String> parameters = new HashMap<String, String>(property.getParameters().size()); Iterator it = property.getParameters().iterator(); while(it.hasNext()) { Parameter parameter = (Parameter) it.next(); parameters.put(parameter.name, parameter.value); } return parameters; } /** * Creates an ArrayList with the X-Param set extracted from a * com.funambol.common.pim.common.Property object. * * @param property the Property object (containing the X-Param HashMap) * @return a list of X-Param's */ protected List<Parameter> getXParams(Property property) { ArrayList<Parameter> parameters = new ArrayList<Parameter>(); if (property.getXParams() != null && property.getXParams().size() > 0) { Map<String, String> h = property.getXParams(); Iterator<String> it = h.keySet().iterator(); String tag = null; String value = null; Parameter parameter = null; while(it.hasNext()) { tag = new String(it.next()); value = new String(h.get(tag)); parameter = new Parameter(tag, value); parameters.add(parameter); } } return parameters; } /** * Added X-Param to the input list of the property parameters * The buffer iterates throguh the parameters and adds the * start parameter char ';' and then the parameter. * Avoids the add the starting ';' by the caller and delete * the trailing ';' here. * * @param paramList the list of standard param * @param prop the property object * */ protected void addXParams(StringBuffer paramList, Property prop) { if (prop.getXParams() != null && prop.getXParams().size() > 0) { Map<String, String> h = prop.getXParams(); Iterator<String> it = h.keySet().iterator(); String tag = null; String value = null; while(it.hasNext()) { tag = it.next(); value = h.get(tag); // // If tag is the same as value then this tag is handle as // param without value // if (tag.equals(value)) { paramList.append(';').append(tag); } else { paramList.append(';') .append(tag) .append("=\"") .append(value) .append('\"'); } } } } /** * @param label * @param property * @param xvCalendar true only if this property is part of a vCalendar (1.0) * item, false if it is part of an iCalendar (2.0) * @return a representation of the event field */ protected com.funambol.common.pim.model.model.Property composeField(String label, Property property , boolean xvCalendar) { if (property == null || property.getPropertyValueAsString() == null) { return null; } com.funambol.common.pim.model.model.Property out = null; String name = label; boolean xtag = false; List<Parameter> params = getXParams(property); String value = new String(property.getPropertyValueAsString()); value = addCRBeforeEachLF(value); boolean qpNeeded = false; // Check if QP-encoding is needed (iCalendar is never QP-encoded) if (xvCalendar && isQPProperty(name)) { try { qpNeeded = true; value = QuotedPrintable.encode(value, charset); if (value.indexOf('=') == -1) { qpNeeded = false; // The encoding was useless } } catch (UnsupportedEncodingException uee) { // It shouldn't happen // The value won't be encoded value = new String(property.getPropertyValueAsString()); qpNeeded = false; } } if (qpNeeded) { // QP codec has been used to encode the value // Escapes only commas, semi-colons and backslashes value = vCalEscapeButKeepNewlines(value); params.add(new Parameter("ENCODING", "QUOTED-PRINTABLE")); params.add(new Parameter("CHARSET", charset)); } else if (!isComplexProperty(name)) { // Checks if escaping is needed // Escapes also line breaks if (xvCalendar) { value = vCalEscape(value); } else { value = iCalEscape(value); } } if (value != null){ out = new com.funambol.common.pim.model.model.Property( name, xtag, params, value); } return out; } // --------------------------------------------------------- Private Methods /** * Sets on a Calendar object those properties which are shared among * calendars of both types. The properties are set according to the content * of a given VCalendar object. * * @param cal * @param vcal * @throws ConverterException */ private void setCommonProperties(Calendar cal, VCalendar vcal) throws ConverterException { cal.setProdId(decodeField(vcal.getProperty("PRODID"))); cal.setVersion(decodeField(vcal.getProperty("VERSION"))); cal.setCalScale(decodeField(vcal.getProperty("CALSCALE"))); cal.setMethod(decodeField(vcal.getProperty("METHOD"))); } /** * Sets on a VCalendar object those properties which are shared among * calendars of both types. The properties are set according to the content * of a given Calendar object. * * @param vcal * @param cal * @param xvCalendar true only if this property is part of a vCalendar (1.0) * item, false if it is part of an iCalendar (2.0) * @throws ConverterException */ private void setCommonProperties(VCalendar vcal, Calendar cal, boolean xvCalendar) throws ConverterException { if (cal.getProdId() != null){ com.funambol.common.pim.model.model.Property prodId = composeField("PRODID", cal.getProdId(), xvCalendar); if (prodId != null){ vcal.addProperty(prodId); } } if (cal.getVersion() != null){ com.funambol.common.pim.model.model.Property version = composeField("VERSION", cal.getVersion(), xvCalendar); if (version != null){ vcal.addProperty(version); } } if (cal.getCalScale() != null){ com.funambol.common.pim.model.model.Property calscale = composeField("CALSCALE", cal.getCalScale(), xvCalendar); if (calscale != null){ vcal.addProperty(calscale); } } if (cal.getMethod() != null){ com.funambol.common.pim.model.model.Property method = composeField("METHOD", cal.getMethod(), xvCalendar); if (method != null){ vcal.addProperty(method); } } } protected Short decodeShortField( com.funambol.common.pim.model.model.Property property) { if (property == null) { return null; } String value = property.getValue(); if (value == null) { return null; } return new Short(Short.parseShort(value)); } /** * * @param label * @param property * @param allDay * @param xvCalendar * @return */ protected com.funambol.common.pim.model.model.Property composeDateTimeField( String label , PropertyWithTimeZone property , boolean allDay , boolean xvCalendar ) { return composeDateTimeField(label, property, allDay, xvCalendar, false); } protected com.funambol.common.pim.model.model.Property composeDateTimeField( String label , PropertyWithTimeZone property , boolean allDay , boolean xvCalendar , boolean isRecurrence ) { com.funambol.common.pim.model.model.Property p = composeField(label, property, xvCalendar); if (p != null) { try { String propertyTimeZoneID = property.getTimeZone(); boolean hasTimeZone = (propertyTimeZoneID != null); int policy = decidePolicy(allDay , xvCalendar , isRecurrence, hasTimeZone ); if (policy == FLOATING_POLICY) { if (!xvCalendar) { // iCalendar removeTime(p); } } if ((policy == UTC_POLICY) && hasTimeZone) { replaceInDateTime(p , TimeZone.getTimeZone(propertyTimeZoneID), null ); } if (policy == PROPERTY_TZ_POLICY) { replaceInDateTime(p , null , TimeZone.getTimeZone(propertyTimeZoneID)); if (!xvCalendar) { // iCalendar p.setParameter(new Parameter("TZID", propertyTimeZoneID)); } } if (policy == CLIENT_TZ_POLICY) { if (hasTimeZone) { replaceInDateTime(p , TimeZone.getTimeZone(propertyTimeZoneID), timezone ); } replaceInDateTime(p , null , timezone ); if (!xvCalendar) { p.setParameter(new Parameter("TZID", timezone.getID())); } } } catch (ConverterException e) { p.setValue(null); } } return p; } protected Property decodeField( com.funambol.common.pim.model.model.Property property) { if (property == null) { return null; } String value = property.getValue(); if (value == null) { return null; } Property encoded = null; String fieldCharset; Parameter fieldCharsetParameter = property.getParameter("CHARSET"); if (fieldCharsetParameter == null) { fieldCharset = charset; // uses the default charset for the device } else { fieldCharset = fieldCharsetParameter.value; // uses the charset } // specified for this field // Check if decoding is needed Parameter encodingParameter = property.getParameter("ENCODING"); if ((encodingParameter != null) && (ENCODING_QP.equalsIgnoreCase(encodingParameter.value))) { value = QuotedPrintable.decode(value.getBytes(), fieldCharset); } // Check if unescaping is needed if (!isComplexProperty(property.getName())) { value = vCalUnescape(value); } // @todo Implement other encoding types encoded = new Property(value); Map<String, String> parameters = getParameters(property); parameters.remove("ENCODING"); // No, thanks parameters.remove("CHARSET"); // No, thanks encoded.setAltrep((String) parameters.remove("ALTREP")); encoded.setCn((String) parameters.remove("CN")); encoded.setCutype((String) parameters.remove("CUTYPE")); encoded.setDelegatedFrom((String) parameters.remove("DELEGATED-FROM")); encoded.setDelegatedTo((String) parameters.remove("DELEGATED-TO")); encoded.setDir((String) parameters.remove("DIR")); encoded.setGroup((String) parameters.remove("GROUP")); encoded.setLanguage((String) parameters.remove("LANGUAGE")); encoded.setMember((String) parameters.remove("MEMBER")); encoded.setPartstat((String) parameters.remove("PARTSTAT")); encoded.setRelated((String) parameters.remove("RELATED")); encoded.setSentby((String) parameters.remove("SENT-BY")); encoded.setTag((String) parameters.remove("TAG")); encoded.setType((String) parameters.remove("TYPE")); encoded.setValue((String) parameters.remove("VALUE")); encoded.setXParams(parameters); // All we left behind return encoded; } protected PropertyWithTimeZone decodeDateTimeField( com.funambol.common.pim.model.model.Property property, TimeZone fieldTimezone) { Property decodedField = decodeField(property); if (decodedField == null) { return new PropertyWithTimeZone(); } if (fieldTimezone == null) { return new PropertyWithTimeZone(decodeField(property), null); } return new PropertyWithTimeZone(decodeField(property), fieldTimezone.getID()); } //----------------------------------------------------------- Public methods /** * This method is used to recognize those properties that could have a * complex (ie, made of different semicolon-separated and comma-separated * parts) content. For such properties, the content saved into the DB is not * unescaped, in order to keep the difference between "true" (ie, escaped) * commas and semicolons and those used as separators (ie, unescaped). * * @param name the name of the property, as a String * @return true if the property can have, according to the specification, a * complex content and therefore mustn't be (un)escaped */ public static boolean isComplexProperty(String name) { // // Returns true for all properties that could have a complex content // if (("RRULE" ).equals(name) || ("EXRULE" ).equals(name) || ("RDATE" ).equals(name) || ("EXDATE" ).equals(name) || ("AALARM" ).equals(name) || ("PALARM" ).equals(name) || ("DALARM" ).equals(name) || ("MALARM" ).equals(name) || ("CATEGORIES").equals(name) || ("GEO" ).equals(name) ) { return true; } return false; } /** * This method is used to recognize those properties that have been decided * to be encoded with Quoted-Printable. This information is to be used only * in the Calendar-to-VCalendar side of the converter. * * @param name the name of the property, as a String * @return true if the property must be QP-encoded */ public static boolean isQPProperty(String name) { // // Returns false for all properties that are never encoded // if (("DTSTART" ).equals(name) || ("DTEND" ).equals(name) || ("DUE" ).equals(name) || ("DTSTAMP" ).equals(name) || ("CREATED" ).equals(name) || ("DCREATED" ).equals(name) || ("LAST-MODIFIED").equals(name) || ("COMPLETED" ).equals(name) || ("RRULE" ).equals(name) || ("RDATE" ).equals(name) || ("EXDATE" ).equals(name) || ("EXRULE" ).equals(name) ) { return false; } return true; } public static String vCalEscapeButKeepNewlines(String raw) { return (raw .replaceAll("\\\\", "\\\\\\\\") .replaceAll(";", "\\\\;")); } public static String vCalEscape(String raw) { return (raw .replaceAll("\\\\", "\\\\\\\\") .replaceAll("\\r\\n", "\\\\N") .replaceAll("\\n", "\\\\N") .replaceAll(";", "\\\\;")); } public static String iCalEscape(String raw) { return (raw .replaceAll("\\\\", "\\\\\\\\") .replaceAll("\\r\\n", "\\\\N") .replaceAll("\\n", "\\\\N") .replaceAll(",", "\\\\,") .replaceAll(";", "\\\\;")); } public static String vCalUnescape(String raw) { StringBuffer unescaped = new StringBuffer(); for (int i = 0; i < raw.length(); i++) { if (raw.charAt(i) == '\\') { // a backslash char nextChar = raw.charAt(++i); // i is being increased if ((nextChar == 'N') | (nextChar == 'n')) { unescaped.append("\r\n"); continue; } // In the other case, we just use the escaped character } unescaped.append(raw.charAt(i)); } return unescaped.toString(); } public static String addCRBeforeEachLF(String raw) { return (raw .replaceAll("(\\r\\n|\\n|\\r)", "\r\n")); } //---------------------------------------------------------- Private methods private TimeZone matchTimeZone( List<com.funambol.common.pim.model.model.Property> properties, Map<String, TimeZoneHelper> timeZones) { for (com.funambol.common.pim.model.model.Property property : properties) { if (property != null) { Parameter tzIDparameter = property.getParameter("TZID"); if ((tzIDparameter != null) && (tzIDparameter.value != null)) { TimeZoneHelper timezoneHelper = timeZones.get(tzIDparameter.value); if (timezoneHelper != null) { return TimeZone.getTimeZone(timezoneHelper.toID(timezone)); } } } } return null; } /** * Adds the time-zone ID to the list only if it exists and is not yet there. */ private boolean addIfNeeded(PropertyWithTimeZone property, List<String> list) { if (property == null) { return false; } String timeZoneID = property.getTimeZone(); if ((timeZoneID != null) && (timeZoneID.length() != 0)) { for (String otherString : list) { if (otherString.equals(timeZoneID)) { return false; } } list.add(timeZoneID); return true; } return false; } /** * Determines the policy to follow in the format conversion of a date/time * property on the basis of different criteria. */ int decidePolicy(boolean allDay, boolean xvCalendar, boolean isRecurrence, boolean hasTimeZone) { if (allDay) { return FLOATING_POLICY; } if (xvCalendar) { // vCalendar 1.0 if (forceClientLocalTime) { return (timezone != null) ? CLIENT_TZ_POLICY : UTC_POLICY; } if (isRecurrence && hasTimeZone) { return PROPERTY_TZ_POLICY; } return UTC_POLICY; } else { // iCalendar 2.0 if (hasTimeZone) { return PROPERTY_TZ_POLICY; } if (forceClientLocalTime) { return (timezone != null) ? CLIENT_TZ_POLICY : UTC_POLICY; } return UTC_POLICY; } } private void replaceInDateTime(com.funambol.common.pim.model.model.Property p, TimeZone timeZoneIn, TimeZone timeZoneOut) throws ConverterException { Pattern pattern = Pattern.compile(DATE_TIME_REGEX); Matcher matcher = pattern.matcher(p.getValue()); StringBuffer buffer = new StringBuffer(); while (matcher.find()) { boolean lookForUTC = (timeZoneIn == null); boolean foundUTC = matcher.group().endsWith("Z"); if (lookForUTC == foundUTC) { String replacement; if (lookForUTC && (timeZoneOut != null)) { // UTC -> local replacement = handleConversionToLocalDate(matcher.group(), timeZoneOut ); } else if (!lookForUTC && (timeZoneOut == null)) { // local -> UTC replacement = handleConversionToUTCDate(matcher.group(), timeZoneIn ); } else if (!lookForUTC && (timeZoneOut != null)) { // local -> local replacement = handleConversionAcrossTimeZones(matcher.group(), timeZoneIn , timeZoneOut ); } else { // UTC -> floating replacement = matcher.group().replaceFirst("Z", ""); } matcher.appendReplacement(buffer, replacement); } } matcher.appendTail(buffer); p.setValue(buffer.toString()); } private void removeTime(com.funambol.common.pim.model.model.Property p) throws ConverterException { Pattern pattern = Pattern.compile(DATE_OR_DATE_TIME_REGEX); Matcher matcher = pattern.matcher(p.getValue()); StringBuffer buffer = new StringBuffer(); boolean found = false; while (matcher.find()) { found = true; String replacement = matcher.group(1) // only the date .replaceAll("-", ""); // no dashes if ((matcher.group(4) != null) && ("DTEND".equals(p.getName()))) { replacement = TimeUtils.rollOneDay(replacement, true ); // roll on } matcher.appendReplacement(buffer, replacement); } matcher.appendTail(buffer); p.setValue(buffer.toString()); // iCalendar all-day properties must specify VALUE=DATE: if (found) { p.setParameter(new Parameter("VALUE", "DATE")); } } private boolean hasNoTimeZone(PropertyWithTimeZone property) { if (property == null) { return true; } if (property.getTimeZone() == null) { return true; } return false; } }