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