001/*
002 *  Licensed to the Apache Software Foundation (ASF) under one
003 *  or more contributor license agreements.  See the NOTICE file
004 *  distributed with this work for additional information
005 *  regarding copyright ownership.  The ASF licenses this file
006 *  to you under the Apache License, Version 2.0 (the
007 *  "License"); you may not use this file except in compliance
008 *  with the License.  You may obtain a copy of the License at
009 *
010 *        http://www.apache.org/licenses/LICENSE-2.0
011 *
012 *  Unless required by applicable law or agreed to in writing,
013 *  software distributed under the License is distributed on an
014 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 *  KIND, either express or implied.  See the License for the
016 *  specific language governing permissions and limitations
017 *  under the License.
018 */
019
020package org.apache.isis.core.progmodel.facets.value;
021
022import java.text.DateFormat;
023import java.text.ParseException;
024import java.text.SimpleDateFormat;
025import java.util.Calendar;
026import java.util.Date;
027import java.util.Iterator;
028import java.util.List;
029import java.util.Map;
030import java.util.StringTokenizer;
031import java.util.TimeZone;
032
033import com.google.common.collect.Maps;
034
035import org.apache.isis.applib.adapters.EncodingException;
036import org.apache.isis.applib.profiles.Localization;
037import org.apache.isis.core.commons.config.ConfigurationConstants;
038import org.apache.isis.core.commons.config.IsisConfiguration;
039import org.apache.isis.core.metamodel.adapter.ObjectAdapter;
040import org.apache.isis.core.metamodel.facetapi.Facet;
041import org.apache.isis.core.metamodel.facetapi.FacetHolder;
042import org.apache.isis.core.metamodel.facets.object.parseable.TextEntryParseException;
043import org.apache.isis.core.progmodel.facets.object.value.ValueSemanticsProviderAndFacetAbstract;
044import org.apache.isis.core.progmodel.facets.object.value.ValueSemanticsProviderContext;
045import org.apache.isis.core.progmodel.facets.value.date.DateValueFacet;
046
047public abstract class ValueSemanticsProviderAbstractTemporal<T> extends ValueSemanticsProviderAndFacetAbstract<T> implements DateValueFacet {
048
049    /**
050     * Introduced to allow BDD tests to provide a different format string
051     * "mid-flight".
052     */
053    public static void setFormat(final String propertyType, final String formatStr) {
054        FORMATS.get().put(propertyType, formatStr);
055    }
056
057    private final static ThreadLocal<Map<String, String>> FORMATS = new ThreadLocal<Map<String, String>>() {
058        @Override
059        protected java.util.Map<String, String> initialValue() {
060            return Maps.newHashMap();
061        }
062    };
063
064    protected static final String ISO_ENCODING_FORMAT = "iso_encoding";
065    protected static final TimeZone UTC_TIME_ZONE;
066
067    public final static String FORMAT_KEY_PREFIX = ConfigurationConstants.ROOT + "value.format.";
068
069    static {
070        TimeZone timeZone = TimeZone.getTimeZone("Etc/UTC");
071        if (timeZone == null) {
072            timeZone = TimeZone.getTimeZone("UTC");
073        }
074        UTC_TIME_ZONE = timeZone;
075    }
076
077    /**
078     * The facet type, used if not specified explicitly in the constructor.
079     */
080    public static Class<? extends Facet> type() {
081        return DateValueFacet.class;
082    }
083
084    protected static DateFormat createDateFormat(final String mask) {
085        return new SimpleDateFormat(mask);
086    }
087    
088    /**
089     * for encoding always use UTC.
090     */
091    protected static DateFormat createDateEncodingFormat(final String mask) {
092        DateFormat encodingFormat = createDateFormat(mask);
093        encodingFormat.setTimeZone(UTC_TIME_ZONE);
094        return encodingFormat;
095    }
096 
097    private final DateFormat encodingFormat;
098    protected DateFormat format;
099    private String configuredFormat;
100    private String propertyType;
101
102    /**
103     * Uses {@link #type()} as the facet type.
104     */
105    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 IsisConfiguration configuration,
106            final ValueSemanticsProviderContext context) {
107        this(propertyName, type(), holder, adaptedClass, typicalLength, immutability, equalByContent, defaultValue, configuration, context);
108    }
109
110    /**
111     * Allows the specific facet subclass to be specified (rather than use
112     * {@link #type()}.
113     */
114    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,
115            final IsisConfiguration configuration, final ValueSemanticsProviderContext context) {
116        super(facetType, holder, adaptedClass, typicalLength, immutability, equalByContent, defaultValue, configuration, context);
117        configureFormats();
118
119        this.propertyType = propertyType;
120        configuredFormat = getConfiguration().getString(FORMAT_KEY_PREFIX + propertyType, defaultFormat()).toLowerCase().trim();
121        buildFormat(configuredFormat);
122
123        encodingFormat = formats().get(ISO_ENCODING_FORMAT);
124    }
125
126    protected void configureFormats() {
127        final Map<String, DateFormat> formats = formats();
128        for (final Map.Entry<String, DateFormat> mapEntry : formats.entrySet()) {
129            final DateFormat format = mapEntry.getValue();
130            format.setLenient(false);
131            if (ignoreTimeZone()) {
132                format.setTimeZone(UTC_TIME_ZONE);
133            }
134        }
135    }
136
137    protected void buildDefaultFormatIfRequired() {
138        final Map<String, String> map = FORMATS.get();
139        final String currentlyConfiguredFormat = map.get(propertyType);
140        if (currentlyConfiguredFormat == null || configuredFormat.equals(currentlyConfiguredFormat)) {
141            return;
142        }
143
144        // (re)create format
145        configuredFormat = currentlyConfiguredFormat;
146        buildFormat(configuredFormat);
147    }
148
149    protected void buildFormat(final String configuredFormat) {
150        final Map<String, DateFormat> formats = formats();
151        format = formats.get(configuredFormat);
152        if (format == null) {
153            setMask(configuredFormat);
154        }
155    }
156
157    // //////////////////////////////////////////////////////////////////
158    // Parsing
159    // //////////////////////////////////////////////////////////////////
160
161    @Override
162    protected T doParse(final Object context, final String entry, final Localization localization) {
163        buildDefaultFormatIfRequired();
164        final String dateString = entry.trim();
165        final String str = dateString.toLowerCase();
166        if (str.equals("today") || str.equals("now")) {
167            return now();
168        } else if (dateString.startsWith("+")) {
169            return relativeDate(context == null ? now() : context, dateString, true);
170        } else if (dateString.startsWith("-")) {
171            return relativeDate(context == null ? now() : context, dateString, false);
172        } else {
173            return parseDate(dateString, context == null ? now() : context, localization);
174        }
175    }
176
177    private T parseDate(final String dateString, final Object original, final Localization localization) {
178        List<DateFormat> elements = formatsToTry(localization);
179        return setDate(parseDate(dateString, elements.iterator()));
180    }
181
182    protected abstract List<DateFormat> formatsToTry(Localization localization);
183
184    private Date parseDate(final String dateString, final Iterator<DateFormat> elements) {
185        final DateFormat format = elements.next();
186        try {
187            return format.parse(dateString);
188        } catch (final ParseException e) {
189            if (elements.hasNext()) {
190                return parseDate(dateString, elements);
191            } else {
192                throw new TextEntryParseException("Not recognised as a date: " + dateString);
193            }
194        }
195    }
196
197    private T relativeDate(final Object object, final String str, final boolean add) {
198        if (str.equals("")) {
199            return now();
200        }
201
202        try {
203            T date = (T) object;
204            final StringTokenizer st = new StringTokenizer(str.substring(1), " ");
205            while (st.hasMoreTokens()) {
206                final String token = st.nextToken();
207                date = relativeDate2(date, token, add);
208            }
209            return date;
210        } catch (final Exception e) {
211            return now();
212        }
213    }
214
215    private T relativeDate2(final T original, String str, final boolean add) {
216        int hours = 0;
217        int minutes = 0;
218        int days = 0;
219        int months = 0;
220        int years = 0;
221
222        if (str.endsWith("H")) {
223            str = str.substring(0, str.length() - 1);
224            hours = Integer.valueOf(str).intValue();
225        } else if (str.endsWith("M")) {
226            str = str.substring(0, str.length() - 1);
227            minutes = Integer.valueOf(str).intValue();
228        } else if (str.endsWith("w")) {
229            str = str.substring(0, str.length() - 1);
230            days = 7 * Integer.valueOf(str).intValue();
231        } else if (str.endsWith("y")) {
232            str = str.substring(0, str.length() - 1);
233            years = Integer.valueOf(str).intValue();
234        } else if (str.endsWith("m")) {
235            str = str.substring(0, str.length() - 1);
236            months = Integer.valueOf(str).intValue();
237        } else if (str.endsWith("d")) {
238            str = str.substring(0, str.length() - 1);
239            days = Integer.valueOf(str).intValue();
240        } else {
241            days = Integer.valueOf(str).intValue();
242        }
243
244        if (add) {
245            return add(original, years, months, days, hours, minutes);
246        } else {
247            return add(original, -years, -months, -days, -hours, -minutes);
248        }
249    }
250
251    // ///////////////////////////////////////////////////////////////////////////
252    // TitleProvider
253    // ///////////////////////////////////////////////////////////////////////////
254
255    @Override
256    public String titleString(final Object value, final Localization localization) {
257        if (value == null) {
258            return null;
259        }
260        final Date date = dateValue(value);
261        DateFormat f = format;
262        if (localization != null) {
263            f = format(localization);
264        }
265        return titleString(f, date);
266    }
267
268    protected DateFormat format(final Localization localization) {
269        return format;
270    }
271
272    @Override
273    public String titleStringWithMask(final Object value, final String usingMask) {
274        final Date date = dateValue(value);
275        return titleString(new SimpleDateFormat(usingMask), date);
276    }
277
278    private String titleString(final DateFormat formatter, final Date date) {
279        return date == null ? "" : formatter.format(date);
280    }
281
282    // //////////////////////////////////////////////////////////////////
283    // EncoderDecoder
284    // //////////////////////////////////////////////////////////////////
285
286    @Override
287    protected String doEncode(final Object object) {
288        final Date date = dateValue(object);
289        return encode(date);
290    }
291
292    private synchronized String encode(final Date date) {
293        return encodingFormat.format(date);
294    }
295
296    @Override
297    protected T doRestore(final String data) {
298        final Calendar cal = Calendar.getInstance();
299        cal.setTimeZone(UTC_TIME_ZONE);
300
301        // TODO allow restoring of dates where datetime expected, and datetimes where date expected - to allow for changing of field types. 
302        try {
303            cal.setTime(parse(data));
304            clearFields(cal);
305            return setDate(cal.getTime());
306        } catch (final ParseException e) {
307            if (data.charAt(0) == 'T') {
308                final long millis = Long.parseLong(data.substring(1));
309                cal.setTimeInMillis(millis);
310                clearFields(cal);
311                return setDate(cal.getTime());
312            } else {
313                throw new EncodingException(e);
314            }
315        }
316    }
317
318    private synchronized Date parse(final String data) throws ParseException {
319        return encodingFormat.parse(data);
320    }
321
322    // //////////////////////////////////////////////////////////////////
323    // DateValueFacet
324    // //////////////////////////////////////////////////////////////////
325
326    @Override
327    public final Date dateValue(final ObjectAdapter object) {
328        return object == null ? null : dateValue(object.getObject());
329    }
330
331    @Override
332    public final ObjectAdapter createValue(final Date date) {
333        return getAdapterManager().adapterFor(setDate(date));
334    }
335
336
337    // //////////////////////////////////////////////////////////////////
338    // temporal-specific stuff
339    // //////////////////////////////////////////////////////////////////
340
341    protected abstract T add(T original, int years, int months, int days, int hours, int minutes);
342
343    protected void clearFields(final Calendar cal) {
344    }
345
346    protected abstract Date dateValue(Object value);
347
348    protected abstract String defaultFormat();
349
350    protected abstract Map<String, DateFormat> formats();
351
352    protected boolean ignoreTimeZone() {
353        return false;
354    }
355
356    protected abstract T now();
357
358    protected abstract T setDate(Date date);
359
360    public void setMask(final String mask) {
361        format = new SimpleDateFormat(mask);
362        format.setTimeZone(UTC_TIME_ZONE);
363        format.setLenient(false);
364    }
365
366    protected boolean isEmpty() {
367        return false;
368    }
369
370}