/**
* Copyright (c) 2014-2017 by the respective copyright holders.
* 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
*/
package org.eclipse.smarthome.core.scheduler;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;
import org.eclipse.smarthome.core.scheduler.DateExpression.DateExpressionPart;
/**
* <code>DateExpression</code> is an implementation of {@link Expression} that provides a parser and evaluator for for
* ISO8601 date expressions (https://en.wikipedia.org/wiki/ISO_8601). ISO8601 expressions provide the ability to specify
* simple yet precise dates.
* <P>
* ISO8601 expressions are comprised of 7 required fields described as follows:
*
* YYYY-MM-DDThh:mm:ss.sTZD
*
* where
*
* YYYY = four-digit year
* MM = two-digit month (01=January, etc.)
* DD = two-digit day of month (01 through 31)
* hh = two digits of hour (00 through 23) (am/pm NOT allowed)
* mm = two digits of minute (00 through 59)
* ss = two digits of second (00 through 59)
* s = one or more digits representing a decimal fraction of a second
* TZD = time zone designator (Z or +hh:mm or -hh:mm)
*
* IMPORTANT NOTE : ISO8601 expressions that specify fractions of seconds are NOT supported
*
* @author Karel Goderis - Intial Contribution
*
*/
public class DateExpression extends AbstractExpression<DateExpressionPart> {
public DateExpression(final String date) throws ParseException {
this(date, Calendar.getInstance().getTime(), TimeZone.getDefault());
}
public DateExpression(final String dateExpression, final Date startTime, final TimeZone zone)
throws ParseException {
super(dateExpression, "", startTime, zone, 0, 1);
}
@Override
public boolean isSatisfiedBy(Date date) {
if (date == null) {
throw new IllegalArgumentException("Date cannot be null");
}
Calendar testDateCal = Calendar.getInstance(getTimeZone());
testDateCal.setTime(date);
testDateCal.set(Calendar.MILLISECOND, 0);
Date originalDate = testDateCal.getTime();
testDateCal.add(Calendar.SECOND, -1);
Date timeAfter = getTimeAfter(testDateCal.getTime());
return ((timeAfter != null) && (timeAfter.equals(originalDate)));
}
public static boolean isValidExpression(String dateExpression) {
try {
new DateExpression(dateExpression);
} catch (ParseException pe) {
return false;
}
return true;
}
@Override
protected void validateExpression() throws IllegalArgumentException {
// Nothing to do here
}
@Override
protected void populateWithSeeds() {
// Nothing to do here
}
@Override
protected DateExpressionPart parseToken(String token, int position) throws ParseException {
return new DateExpressionPart(token);
}
protected class DateExpressionPart extends AbstractExpressionPart {
private Date theDate;
public DateExpressionPart(String s) throws ParseException {
super(s);
}
public Date getDate() {
return theDate;
}
@Override
public void parse() throws ParseException {
// Unfortunately, the time zone formats available to SimpleDateFormat (Java 6 and earlier) are not ISO 8601
// compliant. SimpleDateFormat understands time zone strings like "GMT+01:00" or "+0100", the latter
// according to RFC # 822.
//
// Even if Java 7 added support for time zone descriptors according to ISO 8601, SimpleDateFormat is still
// not able to properly parse a complete date string, as it has no support for optional parts.
//
// Reformatting the input string using regexp is certainly one possibility, but the replacement rules are
// not as simple
//
// Some time zones are not full hours off UTC, so the string does not necessarily end with ":00".
// ISO8601 allows only the number of hours to be included in the time zone, so "+01" is equivalent to
// "+01:00". ISO8601 allows the usage of "Z" to indicate UTC instead of "+00:00".
// The easier solution is to use the data type converter in JAXB, since JAXB must be able to parse
// ISO8601 date string according to the XML Schema specification.
// try {
// Calendar cal = DatatypeConverter.parseDateTime(getExpression());
// theDate = cal.getTime();
// } catch (Exception e) {
// throw new ParseException(getPart() + " is not an ISO8601 formatted date", 0);
// }
if (!parseFormats(
new String[] { "yyyy-MM-dd'T'HH:mm:ssX", "yyyy-MM-dd'T'HH:mm:ssXX", "yyyy-MM-dd'T'HH:mm:ssXXX" })) {
throw new ParseException(getPart() + " is not an ISO8601 formatted date", 0);
}
}
/**
* Try to parse using a set of valid formats.
*
* @param formats the date format strings in the order they should be used
* @return true if parsing succeeded by a format, otherwise false
*/
private boolean parseFormats(final String[] formats) {
for (final String format : formats) {
try {
final DateFormat df = new SimpleDateFormat(format);
df.setTimeZone(getTimeZone());
theDate = df.parse(getExpression());
return true;
} catch (final Exception e) {
// Try next one...
}
}
return false;
}
@Override
public ArrayList<Date> apply(Date startDate, ArrayList<Date> candidates) {
candidates.add(theDate);
return candidates;
}
@Override
BoundedIntegerSet initializeValueSet() {
return null;
}
@Override
public int order() {
return 1;
}
}
@Override
public boolean hasFloatingStartDate() {
return false;
}
}