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}