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