/******************************************************************************* * Copyright (c) 2005, 2012 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * IBM Corporation - initial API and implementation *******************************************************************************/ package org.eclipse.bpel.ui.util; import java.util.Calendar; import java.util.GregorianCalendar; import java.util.TimeZone; import org.eclipse.bpel.ui.properties.DateTimeSelector; /** * Helpers for interacting with DateTimeSelector and DurationSelector widgets, and * serializing their values to XPath. */ public class BPELDateTimeHelpers { // TODO: order must match the DateTimeSelector and DurationSelector values.. protected static final int YEAR = 0; protected static final int MONTH = 1; protected static final int DAY = 2; protected static final int HOUR = 3; protected static final int MINUTE = 4; protected static final int SECOND = 5; protected static final String[] dateTimeFormats = { "yyyy","MM","dd","HH","mm","ss" //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ }; protected static String dateTimeFormat = "yyyy-MM-ddTHH:mm:ss"; //$NON-NLS-1$ // these values are used to populate the Year combo of the DateTimeSelector. // they are also used directly by some helpers below. public static final int yearMin = 2002; public static final int yearMax = Calendar.getInstance().get(Calendar.YEAR) + 5; // Returns the number of consecutive ASCII zeroes in s beginning at position start. protected static int numZeroes(String s, int start) { int i; for (i = start; i<s.length(); i++) if (s.charAt(i) != '0') break; return i-start; } // Returns the number of consecutive ASCII digits in s beginning at position start. protected static int numDigits(String s, int start) { int i; for (i = start; i<s.length(); i++) { char c = s.charAt(i); if ((c < '0') || (c > '9')) break; } return i-start; } protected static int fQuotient(int a, int b) { return (int)Math.floor((double)a/(double)b); } protected static int modulo(int a, int b) { return a-fQuotient(a,b)*b; } protected static int fQuotient3(int a, int low, int high) { return fQuotient(a-low, high-low); } protected static int modulo3(int a, int low, int high) { return modulo(a-low, high-low) + low; } protected static int maximumDayInMonthFor(int yearValue, int monthValue) { int month = modulo3(monthValue, 1, 13); int year = yearValue + fQuotient3(monthValue, 1, 13); return DateTimeSelector.daysInMonth(month-1, year); } public static int[] dateTimePlusDuration(int[] dateTime, int[] duration) { int[] result = new int[6]; // This algorithm from XML Schema spec: //http://www.w3.org/TR/2001/REC-xmlschema-2-20010502/#adding-durations-to-dateTimes // months int temp = dateTime[MONTH] + duration[MONTH]; result[MONTH] = modulo3(temp, 1, 13); int carry = fQuotient3(temp, 1, 13); // years result[YEAR] = dateTime[YEAR] + duration[YEAR] + carry; // seconds temp = dateTime[SECOND] + duration[SECOND]; result[SECOND] = modulo(temp,60); carry = fQuotient(temp,60); // minutes temp = dateTime[MINUTE] + duration[MINUTE] + carry; result[MINUTE] = modulo(temp,60); carry = fQuotient(temp,60); // hours temp = dateTime[HOUR] + duration[HOUR] + carry; result[HOUR] = modulo(temp,24); carry = fQuotient(temp,24); // days int tempDays; if (dateTime[DAY] > maximumDayInMonthFor(result[YEAR], result[MONTH])) { tempDays = maximumDayInMonthFor(result[YEAR], result[MONTH]); } else if (dateTime[DAY] < 1) { tempDays = 1; } else { tempDays = dateTime[DAY]; } result[DAY] = tempDays + duration[DAY] + carry; for (;;) { if (result[DAY] < 1) { result[DAY] = result[DAY] + maximumDayInMonthFor(result[YEAR],result[MONTH]-1); carry = -1; } else if (result[DAY] > maximumDayInMonthFor(result[YEAR], result[MONTH])) { result[DAY] = result[DAY] - maximumDayInMonthFor(result[YEAR], result[MONTH]); carry = 1; } else break; temp = result[MONTH] + carry; result[MONTH] = modulo3(temp,1,13); result[YEAR] = result[YEAR] + fQuotient3(temp,1,13); } return result; } /** * This method reads an XPath string and tries to parse it into a DateTime. * The string MUST conform to the XML Schema DateTime type or parsing will fail. * Returns an int[6] array if parsing is successful, or null if it fails. * * If the isValidFrom flag is true, any timezone specifier is IGNORED and UTC * is assumed. It will also accept a value WITHOUT QUOTES AROUND IT if this * flag is true. * * TODO: this doesn't support negative fields, fractional seconds or years * greater than Integer.MAX_VALUE. * */ public static int[] parseXPathDateTime(String xpath, boolean isValidFrom) { int[] v = new int[6]; if (xpath == null) return null; String s = removeLiteralQuotes(xpath); if (s == null && !isValidFrom) return null; if (s != null) xpath = s; if (xpath.length() < 19) return null; // The year field must be at least 4 digits (padded with leading zeroes), but if // the year is greater than 999, the year field may not contain leading zeroes. int i = numDigits(xpath,0); if (i<4) return null; int j = numZeroes(xpath,0); if ((i>4) && (j>0)) return null; if (j > 3) return null; // year 0 is not allowed if (xpath.length() < 15+i) return null; try { v[YEAR] = Integer.parseInt(xpath.substring(j,i)); if (v[YEAR] < yearMin || v[YEAR] > yearMax) return null; if (xpath.charAt(i++) != '-') return null; if (numDigits(xpath,i) != 2) return null; v[MONTH] = Integer.parseInt(xpath.substring(i,i+2)); i += 2; if (v[MONTH] < 1 || v[MONTH] > 12) return null; if (xpath.charAt(i++) != '-') return null; if (numDigits(xpath,i) != 2) return null; v[DAY] = Integer.parseInt(xpath.substring(i,i+2)); i += 2; if (v[DAY] < 1 || v[DAY] > DateTimeSelector.daysInMonth(v[MONTH]-1, v[YEAR])) return null; if (xpath.charAt(i++) != 'T') return null; if (numDigits(xpath,i) != 2) return null; v[HOUR] = Integer.parseInt(xpath.substring(i,i+2)); i += 2; if (v[HOUR] < 0 || v[HOUR] > 23) return null; if (xpath.charAt(i++) != ':') return null; if (numDigits(xpath,i) != 2) return null; v[MINUTE] = Integer.parseInt(xpath.substring(i,i+2)); i += 2; if (v[MINUTE] < 0 || v[MINUTE] > 59) return null; if (xpath.charAt(i++) != ':') return null; if (numDigits(xpath,i) != 2) return null; v[SECOND] = Integer.parseInt(xpath.substring(i,i+2)); i += 2; if (v[SECOND] < 0 || v[SECOND] > 59) return null; if (xpath.length() == i) { // no timezone indicator // if isValidFrom is true, we assume it means UTC anyways if (isValidFrom) return v; // treat it as local time--convert it to UTC and return. convertLocalToGMT(v); return v; } if (xpath.charAt(i) == '.') { // discard fractional seconds. ++i; i += numDigits(xpath, i); } int[] delta = new int[6]; boolean neg = false; switch (xpath.charAt(i++)) { case 'Z': // UTC timezone. we're done. return (xpath.length() == i)? v : null; case '-': neg=true; // fall through case '+': if (xpath.length() != i+5) return null; if (numDigits(xpath,i) != 2) return null; delta[HOUR] = Integer.parseInt(xpath.substring(i,i+2)); i += 2; if ((delta[HOUR] < 0) || (delta[HOUR] > 23)) return null; if (xpath.charAt(i++) != ':') return null; if (numDigits(xpath,i) != 2) return null; delta[MINUTE] = Integer.parseInt(xpath.substring(i,i+2)); i += 2; if ((delta[MINUTE] < 0) || (delta[MINUTE] > 59)) return null; break; } if (neg) delta[HOUR] = -delta[HOUR]; if (xpath.length() != i) return null; if (isValidFrom) { // if isValidFrom is true, we ignore any specified timezone! return v; } return dateTimePlusDuration(v,delta); } catch (NumberFormatException e) { return null; } catch (NullPointerException e) { // hack return null; } } /** * This method will return a substring with either matching sinqle quotes or matching * double quotes removed. If there weren't matching quotes, it returns null. */ public static String removeLiteralQuotes(String s) { if (s.length() < 2) return null; if (s.charAt(0) != '\"' || s.charAt(s.length()-1) != '\"') { if (s.charAt(0) != '\'' || s.charAt(s.length()-1) != '\'') { return null; } } return s.substring(1,s.length()-1); } /** * This method formats an array int[6] of dateTime values into an XPath string. * The resulting string complies with the XML Schema DateTime type. * If the values are invalid and can't be formatted, null is returned. * * If the isValidFrom flag is true, the UTC specifier "Z" is not appended, * and literal quotes are not added around the text. */ public static String createXPathDateTime(int[] dateTime, boolean isValidFrom) { StringBuffer result = new StringBuffer(dateTimeFormat); for (int i = 0; i<6; i++) { String s = String.valueOf(dateTime[i]); if (s.length() < ((i==YEAR)?4:2)) s = "0"+s; //$NON-NLS-1$ setValueToStringBuffer(dateTimeFormat, result, s, dateTimeFormats[i]); } if (isValidFrom) return result.toString(); return "\'"+result.toString()+"Z\'"; //$NON-NLS-1$ //$NON-NLS-2$ } /** * Returns an int[6] array representing the current DateTime in the local timezone. */ public static int[] getCurrentLocalDateTime() { int[] dateTime = new int[6]; Calendar local = new GregorianCalendar(); dateTime[YEAR] = local.get(Calendar.YEAR); dateTime[MONTH] = local.get(Calendar.MONTH)+1; dateTime[DAY] = local.get(Calendar.DATE); dateTime[HOUR] = local.get(Calendar.HOUR_OF_DAY); dateTime[MINUTE] = local.get(Calendar.MINUTE); dateTime[SECOND] = local.get(Calendar.SECOND); return dateTime; } /** * Use this method on values returned from a DateTimeSelector, if you want the * selector to show local time but serialize to GMT. */ public static void convertLocalToGMT(int[] dateTime) { // convert local timezone to GMT. Calendar local = new GregorianCalendar(); local.set(dateTime[YEAR],dateTime[MONTH],dateTime[DAY], dateTime[HOUR],dateTime[MINUTE],dateTime[SECOND]); Calendar GMTcal = new GregorianCalendar(TimeZone.getTimeZone("GMT+0")); //$NON-NLS-1$ GMTcal.setTime(local.getTime()); dateTime[YEAR] = GMTcal.get(Calendar.YEAR); dateTime[MONTH] = GMTcal.get(Calendar.MONTH); dateTime[DAY] = GMTcal.get(Calendar.DATE); dateTime[HOUR] = GMTcal.get(Calendar.HOUR_OF_DAY); dateTime[MINUTE] = GMTcal.get(Calendar.MINUTE); dateTime[SECOND] = GMTcal.get(Calendar.SECOND); } /** * Use this method on values before storing them in a DateTimeSelector, if you * want the selector to show local time but serialize to GMT. */ public static void convertGMTToLocal(int [] dateTime) { // convert GMT to local timezone Calendar GMTcal = new GregorianCalendar(TimeZone.getTimeZone("GMT+0")); //$NON-NLS-1$ GMTcal.set(dateTime[YEAR],dateTime[MONTH],dateTime[DAY], dateTime[HOUR],dateTime[MINUTE],dateTime[SECOND]); Calendar local = new GregorianCalendar(); local.setTime(GMTcal.getTime()); dateTime[YEAR] = local.get(Calendar.YEAR); dateTime[MONTH] = local.get(Calendar.MONTH); dateTime[DAY] = local.get(Calendar.DATE); dateTime[HOUR] = local.get(Calendar.HOUR_OF_DAY); dateTime[MINUTE] = local.get(Calendar.MINUTE); dateTime[SECOND] = local.get(Calendar.SECOND); } // private static String getValueFromString(String format, String dateString, String unit){ // int beginIdx = format.indexOf(unit); // if (beginIdx < 0) return null; // int endIdx = beginIdx + unit.length(); // if (beginIdx > dateString.length() || endIdx > dateString.length()) return null; // return dateString.substring(beginIdx, endIdx); // } private static void setValueToStringBuffer(String format, StringBuffer dateBuffer, String value, String unit) { int beginIdx = format.indexOf(unit); if (beginIdx < 0) return; int endIdx = beginIdx + unit.length(); dateBuffer.replace(beginIdx, endIdx, value); } /** * This method reads an XPath string and tries to parse it into a Duration. * The string MUST conform to the XML Schema Duration type or parsing will fail. * Returns an int[6] array if parsing is successful, or null if it fails. * * Note that the string should begin and end with a single quote. */ public static int[] parseXPathDuration(String dtStr) { if (dtStr == null) return null; dtStr = removeLiteralQuotes(dtStr); if (dtStr == null) return null; int pos = 0; if (dtStr.length() < 3) return null; if (dtStr.charAt(pos++) != 'P') return null; if (dtStr.endsWith("P") || dtStr.endsWith("T")) return null; //$NON-NLS-1$ //$NON-NLS-2$ int[] value = new int[6]; StringBuffer digits = new StringBuffer(); boolean timePart = false; for (int i = 0; i<6; i++) { if (pos == dtStr.length()) break; if (dtStr.charAt(pos) == 'T') { pos++; if (timePart) return null; timePart = true; } digits.setLength(0); while (Character.isDigit(dtStr.charAt(pos))) { digits.append(dtStr.charAt(pos++)); // We reached the end of the string without finding another letter. if (pos >= dtStr.length()) return null; } // Skip unused terms. Note that M is used both in the date part and the time part. switch (dtStr.charAt(pos++)) { case 'Y': if (timePart || i>0) return null; i = 0; break; case 'M': if (i > (timePart?4:1)) return null; i = timePart?4:1; break; case 'D': if (timePart || i>2) return null; i = 2; break; case 'H': if (!timePart || i>3) return null; i = 3; break; case 'S': if (!timePart || i>5) return null; i = 5; break; default: return null; } try { value[i] = Integer.parseInt(digits.toString()); if (value[i] < 0) return null; } catch (NumberFormatException e) { return null; } } return value; } /** * This method formats an array int[6] of dateTime values into an XPath string. * The resulting string is in ISO 8601 extended format and complies with the * XML Schema Duration type. * If the values are invalid and can't be formatted, null is returned. */ public static String createXPathDuration(int[] duration) { String result = "P"; //$NON-NLS-1$ String spec = "YMDHMS"; //$NON-NLS-1$ boolean timePart = false; for (int i = 0; i<6; i++) { int nv = duration[i]; if (nv > 0) { if (i >= 3 && !timePart) { timePart = true; result += "T"; } //$NON-NLS-1$ result += nv; result += spec.charAt(i); } } return ("P".equals(result))? "\'P0D\'" : "\'" + result + "\'"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ } }