From 3e0bb943518350fcdb358b9783d827d265c3884c Mon Sep 17 00:00:00 2001 From: Nick Burch Date: Wed, 14 Dec 2011 02:23:18 +0000 Subject: [PATCH] ALF-11562 Support for building Java TZ objects from non-DST iCal timezones, with tests git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@32748 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- .../repo/calendar/CalendarHelpersTest.java | 23 ++ .../calendar/CalendarRecurrenceHelper.java | 23 +- .../cmr/calendar/CalendarTimezoneHelper.java | 244 ++++++++++++++++++ 3 files changed, 278 insertions(+), 12 deletions(-) create mode 100644 source/java/org/alfresco/service/cmr/calendar/CalendarTimezoneHelper.java diff --git a/source/java/org/alfresco/repo/calendar/CalendarHelpersTest.java b/source/java/org/alfresco/repo/calendar/CalendarHelpersTest.java index cdd9289930..60f8560504 100644 --- a/source/java/org/alfresco/repo/calendar/CalendarHelpersTest.java +++ b/source/java/org/alfresco/repo/calendar/CalendarHelpersTest.java @@ -21,6 +21,7 @@ package org.alfresco.repo.calendar; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertNotNull; import java.text.SimpleDateFormat; import java.util.ArrayList; @@ -29,11 +30,13 @@ import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.SimpleTimeZone; import java.util.TimeZone; import org.alfresco.service.cmr.calendar.CalendarEntryDTO; import org.alfresco.service.cmr.calendar.CalendarRecurrenceHelper; import org.alfresco.service.cmr.calendar.CalendarService; +import org.alfresco.service.cmr.calendar.CalendarTimezoneHelper; import org.junit.Test; /** @@ -779,6 +782,26 @@ public class CalendarHelpersTest assertEquals("2011-10-21", dateFmt.format(dates.get(1))); assertEquals("2012-01-20", dateFmt.format(dates.get(2))); } + + /** + * Checks we correctly build the Timezone for somewhere + * that doesn't have DST (eg Brisbane) + */ + @Test public void simpleTimezoneNoDST() + { + SimpleTimeZone tz = CalendarTimezoneHelper.buildTimeZone(ICAL_TZ_BRISBANE); + + assertNotNull(tz); + assertEquals("Brisbane", tz.getID()); + + // Doesn't do DST + assertEquals(false, tz.useDaylightTime()); + + // Always 10 hours ahead + assertEquals(10*60*60*1000, tz.getOffset(date(2011,3,1).getTime())); + assertEquals(10*60*60*1000, tz.getOffset(date(2011,9,1).getTime())); + assertEquals(10*60*60*1000, tz.getOffset(date(2011,11,1).getTime())); + } private static class RecurrenceHelper extends CalendarRecurrenceHelper { diff --git a/source/java/org/alfresco/service/cmr/calendar/CalendarRecurrenceHelper.java b/source/java/org/alfresco/service/cmr/calendar/CalendarRecurrenceHelper.java index 9c3ffcb653..e226dbe58b 100644 --- a/source/java/org/alfresco/service/cmr/calendar/CalendarRecurrenceHelper.java +++ b/source/java/org/alfresco/service/cmr/calendar/CalendarRecurrenceHelper.java @@ -45,18 +45,17 @@ public class CalendarRecurrenceHelper { private static Log logger = LogFactory.getLog(CalendarRecurrenceHelper.class); - private static Map d2cd; - static - { - d2cd = new HashMap(); - d2cd.put("SU", Calendar.SUNDAY); - d2cd.put("MO", Calendar.MONDAY); - d2cd.put("TU", Calendar.TUESDAY); - d2cd.put("WE", Calendar.WEDNESDAY); - d2cd.put("TH", Calendar.THURSDAY); - d2cd.put("FR", Calendar.FRIDAY); - d2cd.put("SA", Calendar.SATURDAY); - } + @SuppressWarnings("serial") + protected final static Map d2cd = + Collections.unmodifiableMap(new HashMap() {{ + put("SU", Calendar.SUNDAY); + put("MO", Calendar.MONDAY); + put("TU", Calendar.TUESDAY); + put("WE", Calendar.WEDNESDAY); + put("TH", Calendar.THURSDAY); + put("FR", Calendar.FRIDAY); + put("SA", Calendar.SATURDAY); + }}); /** * The lookup from the day strings to Calendar Day entries diff --git a/source/java/org/alfresco/service/cmr/calendar/CalendarTimezoneHelper.java b/source/java/org/alfresco/service/cmr/calendar/CalendarTimezoneHelper.java new file mode 100644 index 0000000000..96d4abb096 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/calendar/CalendarTimezoneHelper.java @@ -0,0 +1,244 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.service.cmr.calendar; + +import java.util.HashMap; +import java.util.Map; +import java.util.SimpleTimeZone; +import java.util.Stack; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * This class provides helper functions for when working with Timezones + * for Calendar events. + * It provides support for generating iCal timezone information blocks, + * and building Java TimeZones based on iCal timezone information. + * + * @author Nick Burch + * @since 4.0 + */ +public class CalendarTimezoneHelper +{ + private static Log logger = LogFactory.getLog(CalendarTimezoneHelper.class); + + private static final String ICAL_SECTION_EVENT = "VEVENT"; + private static final String ICAL_SECTION_TIMEZONE = "VTIMEZONE"; + private static final String ICAL_SECTION_TZ_STANDARD = "STANDARD"; + private static final String ICAL_SECTION_TZ_DAYLIGHT = "DAYLIGHT"; + +// public static String toICalTimeZone() +// { +// // TODO +// return null; +// } + + /** + * Builds a Java TimeZone from the VTIMEZONE info in an + * iCal file. + * @return a Java TimeZone that matches the iCal one, or NULL if no TZ info present + */ + public static SimpleTimeZone buildTimeZone(String ical) + { + return buildTimeZone( getICalParams(ical) ); + } + + /** + * Internal version that takes the parameters from {@link #getICalParams(String)} + * and builds a TimeZone from it. + * This is not public as it will be refactored when {@link #getICalParams(String)} + * is replaced. + * Note - because it uses the icalParams, we can't handle cases where we're + * given historic TZ info (eg until 2004 it was that, now it's this) + */ + protected static SimpleTimeZone buildTimeZone(Map icalParams) + { + // Pull out the interesting TZ parts + Map tzCore = new HashMap(); + Map tzStandard = new HashMap(); + Map tzDaylight = new HashMap(); + + for (String key : icalParams.keySet()) + { + if (key.startsWith("TZ-")) + { + String value = icalParams.get(key); + + // Assign + key = key.substring(3); + Map dst = tzCore; + + if (key.startsWith(ICAL_SECTION_TZ_STANDARD)) + { + dst = tzStandard; + key = key.substring(ICAL_SECTION_TZ_STANDARD.length()+1); + } + else if (key.startsWith(ICAL_SECTION_TZ_DAYLIGHT)) + { + dst = tzDaylight; + key = key.substring(ICAL_SECTION_TZ_DAYLIGHT.length()+1); + } + + dst.put(key, value); + } + } + + // Do we have any timezone info? + if (tzStandard.isEmpty() && tzDaylight.isEmpty()) + { + logger.warn("No Standard/Daylight info found for " + tzCore); + return null; + } + + // Grab the name of it + String tzID = tzCore.get("TZID"); + if (tzID == null || tzID.isEmpty()) + { + tzID = "(unknown)"; + } + tzID.replaceAll("\\\\", ""); + + // Does it have daylight savings? + if (tzDaylight.isEmpty()) + { + // Life is easy! + int offset = getOffset(tzStandard.get("TZOFFSETTO")); + return new SimpleTimeZone(offset, tzID); + } + + // TODO + return null; + } + + /** + * Turns an iCal offset like "+1000" or "-0730" into an + * offset in milliseconds from UTC + */ + private static int getOffset(String tzOffset) + { + int sign = 1; + + // + or - from UTC? + if (tzOffset.startsWith("+")) + { + sign = 1; + tzOffset = tzOffset.substring(1); + } + else if (tzOffset.startsWith("-")) + { + sign = -1; + tzOffset = tzOffset.substring(1); + } + + int mins = Integer.parseInt( tzOffset.substring(tzOffset.length()-2)); + int hours = Integer.parseInt(tzOffset.substring(0, tzOffset.length()-2)); + + int offset = ((hours*60) + mins) * 60 * 1000; + offset = offset * sign; + + return offset; + } + + /** + * Turns an iCal event into event + timezone parameters. + * This is very closely tied to the SPP / VTI implementation, + * and should be replaced with something more general. + * Until then, it is deliberately not public. + * + * @param params iCal params for the event, and the TZ (prefixed) + */ + protected static Map getICalParams(String icalText) + { + // Split the iCal file by lines + String[] segregatedLines = icalText.split("\r\n"); + if (segregatedLines.length == 1 && icalText.indexOf('\n') > 0) + { + segregatedLines = icalText.split("\n"); + } + + // Perform a stack based parsing of it + Map result = new HashMap(); + int attendeeNum = 0; + Stack stack = new Stack(); + for (String line : segregatedLines) + { + String[] keyValue = line.split(":"); + if (keyValue.length >= 2) + { + if (keyValue[0].equals("BEGIN")) + { + stack.push(keyValue[1]); + continue; + } + if (keyValue[0].equals("END")) + { + stack.pop(); + continue; + } + + if (!stack.isEmpty() && stack.peek().equals(ICAL_SECTION_EVENT)) + { + if (keyValue[0].contains(";")) + { + // Capture the extra details as suffix keys, they're sometimes needed + int splitAt = keyValue[0].indexOf(';'); + String mainKey = keyValue[0].substring(0, splitAt); + + if (splitAt < keyValue[0].length() - 2) + { + // Grab each ;k=v part and store as mainkey-k=v + String[] extras = keyValue[0].substring(splitAt+1).split(";"); + for (String extra : extras) + { + splitAt = extra.indexOf('='); + if (splitAt > -1) + { + result.put(mainKey+"-"+extra.substring(0,splitAt-1), extra.substring(splitAt+1)); + } + } + } + + // Use the main key for the core value + keyValue[0] = mainKey; + } + if (keyValue[0].equals("ATTENDEE")) + { + keyValue[0] = keyValue[0] + attendeeNum; + attendeeNum++; + } + result.put(keyValue[0], keyValue[keyValue.length - 1]); + } + + if (!stack.isEmpty() && stack.peek().equals(ICAL_SECTION_TIMEZONE)) + { + // Store the top level timezone details with a TZ prefix + result.put("TZ-"+keyValue[0], keyValue[keyValue.length-1]); + } + if (stack.size() >= 2 && stack.get(stack.size()-2).equals(ICAL_SECTION_TIMEZONE) && + (stack.peek().equals(ICAL_SECTION_TZ_STANDARD) || stack.peek().equals(ICAL_SECTION_TZ_DAYLIGHT)) ) + { + // Store the timezone details with a TZ prefix + details type + result.put("TZ-"+stack.peek()+"-"+keyValue[0], keyValue[keyValue.length-1]); + } + } + } + return result; + } +}