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 }