/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.stanbol.entityhub.core.utils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.EnumSet;
import javax.xml.datatype.DatatypeConfigurationException;
import javax.xml.datatype.DatatypeFactory;
import javax.xml.datatype.Duration;
import javax.xml.datatype.XMLGregorianCalendar;
import org.apache.stanbol.entityhub.servicesapi.defaults.DataTypeEnum;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.ISODateTimeFormat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Utilities to parse/format Date and Time values for java {@link Date} objects.<p>
* Note that <ul>
* <li> all toString(..) methods format the parsed date in with time zone UTC
* <li> all toDate(..) methods correctly parse dates in any time zone
* <li> for {@link DataTypeEnum#DateTime} a parser with Date + optional Time is
* used. Meaning that also dates with missing Time Element are excepted
* </ul>
*
* @author Rupert Westenthaler
*
*/
public final class TimeUtils {
private TimeUtils(){}
protected static final Logger log = LoggerFactory.getLogger(TimeUtils.class);
/**
* Holds all the data types that represent a date or a time!
*/
private static final EnumSet<DataTypeEnum> dateOrTimeDataTypes =
EnumSet.of(DataTypeEnum.DateTime, DataTypeEnum.Time, DataTypeEnum.Date);
/**
* ShortNames of the supported date or time dataTypes. Only used to write
* meaning full error messages if unsupported data types are parsed!
*/
private static final Collection<String> dateTimeFormatShortNames = new ArrayList<String>(dateOrTimeDataTypes.size());
static {
for(DataTypeEnum dateOrTimeDataType : dateOrTimeDataTypes){
dateTimeFormatShortNames.add(dateOrTimeDataType.getShortName());
}
}
/**
* Used to encode XML DateTime strings with UTC time zone as used for
* {@link DataTypeEnum#DateTime}
*/
protected static final DateTimeFormatter XML_DATE_TIME_FORMAT = ISODateTimeFormat.dateTime().withZone(DateTimeZone.UTC);
/**
* The strict xsd:dateTime parser. It accepts only dateTimes that define
* all elements. Only the "fraction of second" part and the time zone
* are optional.<p>
* This parser is used for {@link DataTypeEnum#DateTime} if
* <code>strict=true</code> (default is <code>false</code>)
*/
protected static final DateTimeFormatter XML_DATE_TIME_PARSER_STRICT = ISODateTimeFormat.basicDateTime();
/**
* The default parser for {@link DataTypeEnum#DateTime}.<p>
* This parser not confirm to xsd:dateTime - that requires both date and time
* to be present - however the parsed value will be the beginning of the
* period (e.g. 2010-05-05 will be parsed to 2010-05-05T00:00:00.000Z).
* This is usually the intension of users that uses dateTimes with a missing
* time. One can parse <code>strict=true</code> to use the
* {@link #XML_DATE_TIME_PARSER_STRICT} instead.
*/
protected static final DateTimeFormatter XML_DATE_TIME_PARSER = ISODateTimeFormat.dateOptionalTimeParser();
/**
* Used to parse dateTime, date or time from a string<p>
* Based on the documentation of JodaTime this accepts all possible XML DateTimes
* <code><pre>
* datetime = time | date-opt-time
* time = 'T' time-element [offset]
* date-opt-time = date-element ['T' [time-element] [offset]]
* date-element = std-date-element | ord-date-element | week-date-element
* std-date-element = yyyy ['-' MM ['-' dd]]
* ord-date-element = yyyy ['-' DDD]
* week-date-element = xxxx '-W' ww ['-' e]
* time-element = HH [minute-element] | [fraction]
* minute-element = ':' mm [second-element] | [fraction]
* second-element = ':' ss [fraction]
* fraction = ('.' | ',') digit+
* offset = 'Z' | (('+' | '-') HH [':' mm [':' ss [('.' | ',') SSS]]])
* <pre><code>
*/
protected static final DateTimeFormatter XML_DATE_TIME_OR_DATE_OR_TIME_PARSER = ISODateTimeFormat.dateTimeParser();
/**
* Used to encode XML DateTime strings without milliseconds with UTC time zone.<p>
* This can be use full if writing MPEG-7 times, because this std. uses a
* Format that allows fractions other than milliseconds
*/
protected static final DateTimeFormatter XML_DATE_TIME_FORMAT_noMillis = ISODateTimeFormat.dateTimeNoMillis().withZone(DateTimeZone.UTC);
/**
* used to encode XML Time string with UTC time zone as used by
* {@link DataTypeEnum#Time}
*/
protected static final DateTimeFormatter XML_TIME_FORMAT = ISODateTimeFormat.time().withZone(DateTimeZone.UTC);
/**
* Used to parse String claiming to be of type {@link DataTypeEnum#Time}<p>
* Based on the Joda Time documentation this parser accepts:
* <code><pre>
* time = ['T'] time-element [offset]
* time-element = HH [minute-element] | [fraction]
* minute-element = ':' mm [second-element] | [fraction]
* second-element = ':' ss [fraction]
* fraction = ('.' | ',') digit+
* offset = 'Z' | (('+' | '-') HH [':' mm [':' ss [('.' | ',') SSS]]])
* </pre><code>
*/
protected static final DateTimeFormatter XML_TIME_PARSER = ISODateTimeFormat.timeParser();
/**
* used to encode XML Date strings with UTC time zone as used by
* {@link DataTypeEnum#Date};
*/
protected static final DateTimeFormatter XML_DATE_FORMAT = ISODateTimeFormat.date().withZone(DateTimeZone.UTC);
/**
* Used to parse String claiming to be of type {@link DataTypeEnum#Date}<p>
* Based on the Joda Time documentation this parser accepts:
* <code><pre>
* date = date-element ['T' offset]
* date-element = std-date-element | ord-date-element | week-date-element
* std-date-element = yyyy ['-' MM ['-' dd]]
* ord-date-element = yyyy ['-' DDD]
* week-date-element = xxxx '-W' ww ['-' e]
* offset = 'Z' | (('+' | '-') HH [':' mm [':' ss [('.' | ',') SSS]]])
* </pre><code>
*/
protected static final DateTimeFormatter XML_DATE_PARSER = ISODateTimeFormat.dateParser();
// TODO Future support for all XML date and time formats
// protected static final DateTimeFormatter XML_gYear_FORMAT = ISODateTimeFormat.year().withZone(DateTimeZone.UTC);
// protected static final DateTimeFormatter XML_gYearMonth_FORMAT = ISODateTimeFormat.yearMonth().withZone(DateTimeZone.UTC);
/**
* Lazy initialisation to avoid Exceptions if {@link DatatypeConfigurationException}
* is thrown during initialisation of the Utility class.<p>
* Do not access directly! Use {@link #getXmlDataTypeFactory()} instead.
*/
private static DatatypeFactory xmlDatatypeFactory;
/**
* Inits the {@link #xmlDatatypeFactory} if not already done.<p>
* @return the XML datatype factory
* @throws IllegalStateException if a {@link DatatypeConfigurationException}
* is encountered during {@link DatatypeFactory#newInstance()}
*/
private static DatatypeFactory getXmlDataTypeFactory() throws IllegalStateException {
if(xmlDatatypeFactory == null){
try {
xmlDatatypeFactory = DatatypeFactory.newInstance();
} catch (DatatypeConfigurationException e) {
throw new IllegalStateException("Unable to instantiate XML Datatype Factory!",e);
}
}
return xmlDatatypeFactory;
}
public static String toString(String dataTypeUri,Date value){
DataTypeEnum dataType = DataTypeEnum.getDataType(dataTypeUri);
if(dataType == null){
throw new IllegalArgumentException(String.format("Unknown dataType %s",dataType));
}
return toString(dataType,value);
}
public static String toString(DataTypeEnum dataType,Date value){
if(value == null){
throw new IllegalArgumentException("The parsed Date MUST NOT be NULL!");
}
if(dataType == null){
throw new IllegalArgumentException("The parsed DataType MUST NOT be NULL!");
}
if(!dateOrTimeDataTypes.contains(dataType)){
throw new IllegalArgumentException(String.format("The parsed DataType %s is not a date and/or time dataType! Supported dataTypes are %s",
dataType.getShortName(),dateTimeFormatShortNames));
}
if(DataTypeEnum.DateTime == dataType){
return XML_DATE_TIME_FORMAT.print(value.getTime());
} else if(DataTypeEnum.Time == dataType){
return XML_TIME_FORMAT.print(value.getTime());
} else if(DataTypeEnum.Date == dataType){
return XML_DATE_FORMAT.print(value.getTime());
} else {
throw new IllegalArgumentException(String.format("Unsupported but valid Date/Time DataType %s encountered. Pleas report this as a BUG!",dataType));
}
}
/**
* Converts the parsed value to a Date
* @param dataType the dataType of Date that should be parsed form the parsed value
* @param value the value
* @return the date
* @throws IllegalArgumentException if the parsed value can not be converted to a date
*/
public static Date toDate(String dataTypeUri, Object value){
return toDate(dataTypeUri, value, false);
}
/**
* Converts the parsed value to a Date
* @param dataType the dataType of Date that should be parsed form the parsed value
* @param value the value
* @param strict if <code>true</code> than all requirements defined by xsd are
* enforced (e.g. dateTimes without time will throw an exception)
* @return the date
* @throws IllegalArgumentException if the parsed value can not be converted to a date
*/
public static Date toDate(String dataTypeUri, Object value, boolean strict){
DataTypeEnum dataType = DataTypeEnum.getDataType(dataTypeUri);
if(dataType == null){
throw new IllegalArgumentException(String.format("Unknown dataType %s",dataType));
}
return toDate(dataType, value,strict);
}
/**
* Converts the parsed value to a Date
* @param dataType the dataType of Date that should be parsed form the parsed value
* @param value the value
* @return the date
* @throws IllegalArgumentException if the parsed value can not be converted to a date
*/
public static Date toDate(DataTypeEnum dataType, Object value) throws IllegalArgumentException {
return toDate(dataType, value, false);
}
/**
* Converts the parsed value to a Date
* @param dataType the dataType of Date that should be parsed form the parsed value
* @param value the value
* @param strict if <code>true</code> than all requirements defined by xsd are
* enforced (e.g. dateTimes without time will throw an exception)
* @return the date
* @throws IllegalArgumentException if the parsed value can not be converted to a date
*/
public static Date toDate(DataTypeEnum dataType, Object value, boolean strict) throws IllegalArgumentException {
DateTime dateTime = toDateTime(dataType, value, strict);
return dateTime.toDate();
}
/**
* Converts the value to a xml Gregorian calendar by using
* {@link DatatypeFactory#newXMLGregorianCalendar(String)}.
* @param value the value
* @return the parsed instance
* @throws IllegalArgumentException if <code>null</code> is parsed
* @throws IllegalStateException if no {@link DatatypeFactory} could be
* instantiated.
*/
public static XMLGregorianCalendar toXMLCal(Object value) throws IllegalArgumentException,IllegalStateException{
if(value == null){
throw new IllegalArgumentException("The parsed value MUST NOT be NULL!");
}
return getXmlDataTypeFactory().newXMLGregorianCalendar(value.toString());
}
public static Duration toDuration(Object value) throws IllegalArgumentException,IllegalStateException{
return toDuration(value,false);
}
public static Duration toDuration(Object value,boolean nullAsZeroDuration) throws IllegalArgumentException,IllegalStateException{
if(value == null){
if(nullAsZeroDuration){
return getXmlDataTypeFactory().newDuration(0);
} else {
throw new IllegalArgumentException("The parsed value MUST NOT be NULL. Parse \"boolean nullAsZeroDuration=true\" to enable creation of zero lenght durations for NULL values!");
}
} else {
return getXmlDataTypeFactory().newDuration(value.toString());
}
}
private static DateTime toDateTime(DataTypeEnum dataType, Object value, boolean strict) throws IllegalArgumentException {
if(value == null){
throw new IllegalArgumentException("The parsed Date MUST NOT be NULL!");
}
if(dataType == null){
throw new IllegalArgumentException("The parsed DataType MUST NOT be NULL!");
}
if(!dateOrTimeDataTypes.contains(dataType)){
throw new IllegalArgumentException(String.format("The parsed DataType %s is not a date and/or time dataType! Supported dataTypes are %s",
dataType.getShortName(),dateTimeFormatShortNames));
}
final DateTime dateTime;
if(value instanceof Date){ //NOTE: returns a valid date for non date dataTypes
dateTime = new DateTime(((Date)value).getTime());
} else if(value instanceof DateTime){//NOTE: returns a valid date for non date dataTypes
dateTime = (DateTime)value;
} else {
if(DataTypeEnum.DateTime == dataType){
if(strict){
dateTime = XML_DATE_TIME_PARSER_STRICT.parseDateTime(value.toString());
} else {
dateTime = XML_DATE_TIME_PARSER.parseDateTime(value.toString());
}
} else if(DataTypeEnum.Time == dataType){
dateTime = XML_TIME_PARSER.parseDateTime(value.toString());
} else if(DataTypeEnum.Date == dataType){
dateTime = XML_DATE_PARSER.parseDateTime(value.toString());
} else {
String strValue = value.toString();
log.error(String.format("Unsupported but valid Date/Time DataType %s encountered. Pleas report this as a BUG!",dataType));
log.warn(String.format("Try to use the generic dateTime-or-Time-or-Date parser as fallback for unsupported but valid Date/Time DataType %s and value %s",
dataType,strValue));
dateTime = XML_DATE_TIME_OR_DATE_OR_TIME_PARSER.parseDateTime(strValue);
}
}
return dateTime;
}
}