/*
* 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.isis.core.metamodel.facets.value;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.TimeZone;
import com.google.common.collect.Maps;
import org.apache.isis.applib.adapters.EncodingException;
import org.apache.isis.core.commons.config.ConfigurationConstants;
import org.apache.isis.core.metamodel.adapter.ObjectAdapter;
import org.apache.isis.core.metamodel.facetapi.Facet;
import org.apache.isis.core.metamodel.facetapi.FacetHolder;
import org.apache.isis.core.metamodel.facets.object.parseable.TextEntryParseException;
import org.apache.isis.core.metamodel.facets.object.value.vsp.ValueSemanticsProviderAndFacetAbstract;
import org.apache.isis.core.metamodel.facets.value.date.DateValueFacet;
import org.apache.isis.core.metamodel.services.ServicesInjector;
public abstract class ValueSemanticsProviderAbstractTemporal<T> extends ValueSemanticsProviderAndFacetAbstract<T> implements DateValueFacet {
/**
* Introduced to allow BDD tests to provide a different format string
* "mid-flight".
*/
public static void setFormat(final String propertyType, final String formatStr) {
FORMATS.get().put(propertyType, formatStr);
}
private final static ThreadLocal<Map<String, String>> FORMATS = new ThreadLocal<Map<String, String>>() {
@Override
protected java.util.Map<String, String> initialValue() {
return Maps.newHashMap();
}
};
protected static final String ISO_ENCODING_FORMAT = "iso_encoding";
protected static final TimeZone UTC_TIME_ZONE;
public final static String FORMAT_KEY_PREFIX = ConfigurationConstants.ROOT + "value.format.";
static {
TimeZone timeZone = TimeZone.getTimeZone("Etc/UTC");
if (timeZone == null) {
timeZone = TimeZone.getTimeZone("UTC");
}
UTC_TIME_ZONE = timeZone;
}
/**
* The facet type, used if not specified explicitly in the constructor.
*/
public static Class<? extends Facet> type() {
return DateValueFacet.class;
}
protected static DateFormat createDateFormat(final String mask) {
return new SimpleDateFormat(mask);
}
/**
* for encoding always use UTC.
*/
protected static DateFormat createDateEncodingFormat(final String mask) {
DateFormat encodingFormat = createDateFormat(mask);
encodingFormat.setTimeZone(UTC_TIME_ZONE);
return encodingFormat;
}
private final DateFormat encodingFormat;
protected DateFormat format;
private String configuredFormat;
private String propertyType;
/**
* Uses {@link #type()} as the facet type.
*/
public ValueSemanticsProviderAbstractTemporal(final String propertyName, final FacetHolder holder, final Class<T> adaptedClass, final int typicalLength, final Immutability immutability, final EqualByContent equalByContent, final T defaultValue,
final ServicesInjector context) {
this(propertyName, type(), holder, adaptedClass, typicalLength, immutability, equalByContent, defaultValue, context);
}
/**
* Allows the specific facet subclass to be specified (rather than use
* {@link #type()}.
*/
public ValueSemanticsProviderAbstractTemporal(final String propertyType, final Class<? extends Facet> facetType, final FacetHolder holder, final Class<T> adaptedClass, final int typicalLength, final Immutability immutability, final EqualByContent equalByContent, final T defaultValue,
final ServicesInjector context) {
super(facetType, holder, adaptedClass, typicalLength, null, immutability, equalByContent, defaultValue, context);
configureFormats();
this.propertyType = propertyType;
configuredFormat = getConfiguration().getString(FORMAT_KEY_PREFIX + propertyType, defaultFormat()).toLowerCase().trim();
buildFormat(configuredFormat);
encodingFormat = formats().get(ISO_ENCODING_FORMAT);
}
protected void configureFormats() {
final Map<String, DateFormat> formats = formats();
for (final Map.Entry<String, DateFormat> mapEntry : formats.entrySet()) {
final DateFormat format = mapEntry.getValue();
format.setLenient(false);
if (ignoreTimeZone()) {
format.setTimeZone(UTC_TIME_ZONE);
}
}
}
protected void buildDefaultFormatIfRequired() {
final Map<String, String> map = FORMATS.get();
final String currentlyConfiguredFormat = map.get(propertyType);
if (currentlyConfiguredFormat == null || configuredFormat.equals(currentlyConfiguredFormat)) {
return;
}
// (re)create format
configuredFormat = currentlyConfiguredFormat;
buildFormat(configuredFormat);
}
protected void buildFormat(final String configuredFormat) {
final Map<String, DateFormat> formats = formats();
format = formats.get(configuredFormat);
if (format == null) {
setMask(configuredFormat);
}
}
// //////////////////////////////////////////////////////////////////
// Parsing
// //////////////////////////////////////////////////////////////////
@Override
protected T doParse(
final String entry,
final Object context) {
buildDefaultFormatIfRequired();
final String dateString = entry.trim();
final String str = dateString.toLowerCase();
if (str.equals("today") || str.equals("now")) {
return now();
} else if (dateString.startsWith("+")) {
return relativeDate(context == null ? now() : context, dateString, true);
} else if (dateString.startsWith("-")) {
return relativeDate(context == null ? now() : context, dateString, false);
} else {
return parseDate(dateString);
}
}
private T parseDate(final String dateString) {
List<DateFormat> elements = formatsToTry();
return setDate(parseDate(dateString, elements.iterator()));
}
protected abstract List<DateFormat> formatsToTry();
private Date parseDate(final String dateString, final Iterator<DateFormat> elements) {
final DateFormat format = elements.next();
try {
return format.parse(dateString);
} catch (final ParseException e) {
if (elements.hasNext()) {
return parseDate(dateString, elements);
} else {
throw new TextEntryParseException("Not recognised as a date: " + dateString);
}
}
}
private T relativeDate(final Object object, final String str, final boolean add) {
if (str.equals("")) {
return now();
}
try {
T date = (T) object;
final StringTokenizer st = new StringTokenizer(str.substring(1), " ");
while (st.hasMoreTokens()) {
final String token = st.nextToken();
date = relativeDate2(date, token, add);
}
return date;
} catch (final Exception e) {
return now();
}
}
private T relativeDate2(final T original, String str, final boolean add) {
int hours = 0;
int minutes = 0;
int days = 0;
int months = 0;
int years = 0;
if (str.endsWith("H")) {
str = str.substring(0, str.length() - 1);
hours = Integer.valueOf(str).intValue();
} else if (str.endsWith("M")) {
str = str.substring(0, str.length() - 1);
minutes = Integer.valueOf(str).intValue();
} else if (str.endsWith("w")) {
str = str.substring(0, str.length() - 1);
days = 7 * Integer.valueOf(str).intValue();
} else if (str.endsWith("y")) {
str = str.substring(0, str.length() - 1);
years = Integer.valueOf(str).intValue();
} else if (str.endsWith("m")) {
str = str.substring(0, str.length() - 1);
months = Integer.valueOf(str).intValue();
} else if (str.endsWith("d")) {
str = str.substring(0, str.length() - 1);
days = Integer.valueOf(str).intValue();
} else {
days = Integer.valueOf(str).intValue();
}
if (add) {
return add(original, years, months, days, hours, minutes);
} else {
return add(original, -years, -months, -days, -hours, -minutes);
}
}
// ///////////////////////////////////////////////////////////////////////////
// TitleProvider
// ///////////////////////////////////////////////////////////////////////////
@Override
public String titleString(final Object value) {
if (value == null) {
return null;
}
final Date date = dateValue(value);
final DateFormat f = format();
return titleString(f, date);
}
protected DateFormat format() {
return format;
}
@Override
public String titleStringWithMask(final Object value, final String usingMask) {
final Date date = dateValue(value);
return titleString(new SimpleDateFormat(usingMask), date);
}
private String titleString(final DateFormat formatter, final Date date) {
return date == null ? "" : formatter.format(date);
}
// //////////////////////////////////////////////////////////////////
// EncoderDecoder
// //////////////////////////////////////////////////////////////////
@Override
protected String doEncode(final Object object) {
final Date date = dateValue(object);
return encode(date);
}
private synchronized String encode(final Date date) {
return encodingFormat.format(date);
}
@Override
protected T doRestore(final String data) {
final Calendar cal = Calendar.getInstance();
cal.setTimeZone(UTC_TIME_ZONE);
// TODO allow restoring of dates where datetime expected, and datetimes where date expected - to allow for changing of field types.
try {
cal.setTime(parse(data));
clearFields(cal);
return setDate(cal.getTime());
} catch (final ParseException e) {
if (data.charAt(0) == 'T') {
final long millis = Long.parseLong(data.substring(1));
cal.setTimeInMillis(millis);
clearFields(cal);
return setDate(cal.getTime());
} else {
throw new EncodingException(e);
}
}
}
private synchronized Date parse(final String data) throws ParseException {
return encodingFormat.parse(data);
}
// //////////////////////////////////////////////////////////////////
// DateValueFacet
// //////////////////////////////////////////////////////////////////
@Override
public final Date dateValue(final ObjectAdapter object) {
return object == null ? null : dateValue(object.getObject());
}
@Override
public final ObjectAdapter createValue(final Date date) {
return getAdapterManager().adapterFor(setDate(date));
}
// //////////////////////////////////////////////////////////////////
// temporal-specific stuff
// //////////////////////////////////////////////////////////////////
protected abstract T add(T original, int years, int months, int days, int hours, int minutes);
protected void clearFields(final Calendar cal) {
}
protected abstract Date dateValue(Object value);
protected abstract String defaultFormat();
protected abstract Map<String, DateFormat> formats();
protected boolean ignoreTimeZone() {
return false;
}
protected abstract T now();
protected abstract T setDate(Date date);
public void setMask(final String mask) {
format = new SimpleDateFormat(mask);
format.setTimeZone(UTC_TIME_ZONE);
format.setLenient(false);
}
protected boolean isEmpty() {
return false;
}
}