org.alfresco.service.cmr.calendar.CalendarTimezoneHelper.java Source code

Java tutorial

Introduction

Here is the source code for org.alfresco.service.cmr.calendar.CalendarTimezoneHelper.java

Source

/*
 * #%L
 * Alfresco Repository
 * %%
 * Copyright (C) 2005 - 2016 Alfresco Software Limited
 * %%
 * This file is part of the Alfresco software. 
 * If the software was purchased under a paid Alfresco license, the terms of 
 * the paid license agreement will prevail.  Otherwise, the software is 
 * provided under the following open source license terms:
 * 
 * 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 <http://www.gnu.org/licenses/>.
 * #L%
 */
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<String, String> icalParams) {
        // Pull out the interesting TZ parts
        Map<String, String> tzCore = new HashMap<String, String>();
        Map<String, String> tzStandard = new HashMap<String, String>();
        Map<String, String> tzDaylight = new HashMap<String, String>();

        for (String key : icalParams.keySet()) {
            if (key.startsWith("TZ-")) {
                String value = icalParams.get(key);

                // Assign
                key = key.substring(3);
                Map<String, String> 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)";
        }
        // De-escape commans
        tzID = tzID.replace("\\,", ",");

        // Does it have daylight savings?
        if (tzDaylight.isEmpty()) {
            // Life is easy!
            int offset = getOffset(tzStandard.get("TZOFFSETTO"));
            return new SimpleTimeZone(offset, tzID);
        }

        // Get the offsets
        int stdOffset = getOffset(tzDaylight.get("TZOFFSETFROM"));
        int dstOffset = getOffset(tzDaylight.get("TZOFFSETTO"));

        // Turn the rules into SimpleTimeZone ones
        int[] stdRules = getRuleForSimpleTimeZone(tzStandard.get("RRULE"));
        int[] dstRules = getRuleForSimpleTimeZone(tzDaylight.get("RRULE"));

        // Build it up
        return new SimpleTimeZone(stdOffset, tzID, dstRules[0], dstRules[1], dstRules[2], // When DST starts
                1 * 60 * 60 * 1000, // TODO Pull out the exact change time from DTSTART
                stdRules[0], stdRules[1], stdRules[2], // When DST ends
                2 * 60 * 60 * 1000 // TODO Pull out the exact change time from DTSTART
        );
    }

    /**
     * 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;
    }

    /**
     * Turn an iCal repeating rule like
     *  "FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10" into a SimpleTimeZone rule
     *  like Month=March, StartDay=0, StartDayOfWeek=-Sunday
     * See the JavaDocs of {@link SimpleTimeZone} for how to express
     *  the different requirements in the required int formats
     */
    private static int[] getRuleForSimpleTimeZone(String rule) {
        // Turn the rule into chunks
        Map<String, String> params = new HashMap<String, String>();
        for (String p : rule.split(";")) {
            int splitAt = p.indexOf('=');
            if (splitAt == -1) {
                logger.info("Skipping invalid param " + p + " in recurrence rule " + rule);
            } else {
                params.put(p.substring(0, splitAt), p.substring(splitAt + 1));
            }
        }

        // Java months are 1 less than normal
        int month = Integer.parseInt(params.get("BYMONTH")) - 1;

        // Should end with a day of the week
        String byDay = params.get("BYDAY");
        String dow = byDay.substring(byDay.length() - 2);
        int dayOfWeek = CalendarRecurrenceHelper.d2cd.get(dow);

        // Where in the month does it come?
        int dayOfMonth = 0;
        if (byDay.startsWith("-1")) {
            // Last in month
            dayOfMonth = -1;
        } else if (byDay.startsWith("1")) {
            // First in month
            dayOfMonth = 1;
            dayOfWeek = 0 - dayOfWeek;
        } else {
            // Nth day in month
            dayOfMonth = 1 + (Integer.parseInt(byDay.substring(0, 1)) - 1) * 7;
            dayOfWeek = 0 - dayOfWeek;
        }

        // All done
        return new int[] { month, dayOfMonth, dayOfWeek };
    }

    /**
     * Splits an iCal line into key and value by the first
     * unquoted colon.
     * @param icalLine String
     */
    protected static String[] icalLineKeyValue(String icalLine) {
        int delim = indexOfFirstUnquotedColon(icalLine);
        if (delim == -1) {
            return new String[] { "", "" };
        }
        String key = icalLine.substring(0, delim);
        String value = icalLine.substring(delim + 1);
        return new String[] { key, value };
    }

    /**
     * @param icalLine String
     * @return location of first non quote enclosed colon
     */
    private static int indexOfFirstUnquotedColon(String icalLine) {
        int colon = icalLine.indexOf(":");
        int quote = icalLine.indexOf("\"");

        if (quote == -1) {
            return colon;
        } else {
            if (colon < quote) {
                return colon;
            } else {
                //next quote, skipping past a colon if exists
                int nextQuote = icalLine.indexOf("\"", quote + 1);
                if (nextQuote == -1) {
                    //will only happen if the quotes are unbalanced
                    return -1;
                } else {
                    return nextQuote
                            + indexOfFirstUnquotedColon(icalLine.substring(nextQuote + 1, icalLine.length())) + 1;
                }
            }
        }
    }

    /**
     * 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 icalText iCal text for the event, and the TZ (prefixed)
     */
    protected static Map<String, String> 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<String, String> result = new HashMap<String, String>();
        int attendeeNum = 0;
        Stack<String> stack = new Stack<String>();
        for (String line : segregatedLines) {
            String[] keyValue = icalLineKeyValue(line);
            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.containsKey(mainKey + "-" + extra.substring(0, 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++;
                    }

                    if (!result.containsKey(keyValue[0])) {
                        result.put(keyValue[0], keyValue[keyValue.length - 1]);
                    }
                }

                if (!stack.isEmpty() && stack.peek().equals(ICAL_SECTION_TIMEZONE)
                        && !result.containsKey("TZ-" + keyValue[0])) {
                    // 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))
                        && !result.containsKey("TZ-" + stack.peek() + "-" + keyValue[0])) {
                    // Store the timezone details with a TZ prefix + details type
                    result.put("TZ-" + stack.peek() + "-" + keyValue[0], keyValue[keyValue.length - 1]);
                }
            }
        }
        return result;
    }
}