You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@isis.apache.org by da...@apache.org on 2013/12/13 15:03:48 UTC

[4/4] git commit: ISIS-630: also reimplemented VSP for Joda LocalDateTime

ISIS-630: also reimplemented VSP for Joda LocalDateTime

... so it too no longer has any dependencies on the concept of UTC.


Project: http://git-wip-us.apache.org/repos/asf/isis/repo
Commit: http://git-wip-us.apache.org/repos/asf/isis/commit/192b6b22
Tree: http://git-wip-us.apache.org/repos/asf/isis/tree/192b6b22
Diff: http://git-wip-us.apache.org/repos/asf/isis/diff/192b6b22

Branch: refs/heads/master
Commit: 192b6b229ab21ca9a0a6f3a893fc90272898942a
Parents: b87c2a9
Author: Dan Haywood <da...@apache.org>
Authored: Fri Dec 13 14:03:18 2013 +0000
Committer: Dan Haywood <da...@apache.org>
Committed: Fri Dec 13 14:03:18 2013 +0000

----------------------------------------------------------------------
 .../progmodel/facets/value/JodaFunctions.java   |  55 +++++
 .../value/datejodalocal/JodaLocalDateUtil.java  | 127 ++++++++++
 .../JodaLocalDateValueSemanticsProvider.java    | 221 ++++--------------
 .../JodaLocalDateTimeUtil.java                  | 124 ++++++++++
 ...JodaLocalDateTimeValueSemanticsProvider.java | 232 +++++++++++++++++--
 ...lDateTimeValueSemanticsProviderAbstract.java | 118 ----------
 6 files changed, 574 insertions(+), 303 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/isis/blob/192b6b22/core/metamodel/src/main/java/org/apache/isis/core/progmodel/facets/value/JodaFunctions.java
----------------------------------------------------------------------
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/progmodel/facets/value/JodaFunctions.java b/core/metamodel/src/main/java/org/apache/isis/core/progmodel/facets/value/JodaFunctions.java
new file mode 100644
index 0000000..2ff05e3
--- /dev/null
+++ b/core/metamodel/src/main/java/org/apache/isis/core/progmodel/facets/value/JodaFunctions.java
@@ -0,0 +1,55 @@
+/*
+ *  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.progmodel.facets.value;
+
+import java.util.List;
+import java.util.Locale;
+import java.util.StringTokenizer;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Iterables;
+
+import org.joda.time.LocalDate;
+import org.joda.time.format.DateTimeFormatter;
+
+import org.apache.isis.applib.profiles.Localization;
+import org.apache.isis.core.metamodel.facets.object.parseable.TextEntryParseException;
+
+public final class JodaFunctions  {
+
+    private JodaFunctions(){}
+    
+    public static Function<DateTimeFormatter, DateTimeFormatter> withLocale(final Localization localization) {
+        return new Function<DateTimeFormatter, DateTimeFormatter>() {
+            @Override
+            public DateTimeFormatter apply(DateTimeFormatter input) {
+                if (localization == null) {
+                    return input;
+                } 
+                final Locale locale = localization.getLocale();
+                if (locale == null) {
+                    return input;
+                } 
+                return input.withLocale(locale);
+            }
+        };
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/192b6b22/core/metamodel/src/main/java/org/apache/isis/core/progmodel/facets/value/datejodalocal/JodaLocalDateUtil.java
----------------------------------------------------------------------
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/progmodel/facets/value/datejodalocal/JodaLocalDateUtil.java b/core/metamodel/src/main/java/org/apache/isis/core/progmodel/facets/value/datejodalocal/JodaLocalDateUtil.java
new file mode 100644
index 0000000..ebcd901
--- /dev/null
+++ b/core/metamodel/src/main/java/org/apache/isis/core/progmodel/facets/value/datejodalocal/JodaLocalDateUtil.java
@@ -0,0 +1,127 @@
+/*
+ *  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.progmodel.facets.value.datejodalocal;
+
+import java.util.List;
+import java.util.StringTokenizer;
+
+import com.google.common.collect.Iterables;
+
+import org.joda.time.LocalDate;
+import org.joda.time.format.DateTimeFormatter;
+
+import org.apache.isis.applib.profiles.Localization;
+import org.apache.isis.core.metamodel.facets.object.parseable.TextEntryParseException;
+import org.apache.isis.core.progmodel.facets.value.JodaFunctions;
+
+public final class JodaLocalDateUtil  {
+
+    private JodaLocalDateUtil(){}
+    
+    static LocalDate parseDate(final String dateStr, final Localization localization, List<DateTimeFormatter> parseFormatters) {
+        Iterable<DateTimeFormatter> elements = Iterables.transform(parseFormatters, JodaFunctions.withLocale(localization));
+        LocalDate parsedDate = parseDate(dateStr, elements);
+        return parsedDate;
+    }
+
+    
+    private static LocalDate parseDate(String dateStr, Iterable<DateTimeFormatter> formatters) {
+        for(DateTimeFormatter formatter: formatters) {
+            try {
+                return formatter.parseLocalDate(dateStr);
+            } catch (final IllegalArgumentException e) {
+                // continue to next
+            }
+        }
+        throw new TextEntryParseException("Not recognised as a date: " + dateStr);
+    }
+
+    // //////////////////////////////////////
+
+    static LocalDate relativeDate(final LocalDate contextDate, final String str, final boolean add) {
+        LocalDate relativeDate = contextDate;
+        if (str.equals("")) {
+            return contextDate;
+        }
+
+        try {
+            final StringTokenizer st = new StringTokenizer(str.substring(1), " ");
+            while (st.hasMoreTokens()) {
+                final String token = st.nextToken();
+                relativeDate = adjustDate(relativeDate, token, add);
+            }
+            return relativeDate;
+        } catch (final Exception e) {
+            return contextDate;
+        }
+    }
+
+    private static LocalDate adjustDate(final LocalDate contextDate, 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(contextDate, years, months, days, hours, minutes);
+        } else {
+            return add(contextDate, -years, -months, -days, -hours, -minutes);
+        }
+    }
+
+    private static LocalDate add(final LocalDate original, final int years, final int months, final int days, final int hours, final int minutes) {
+        if(hours != 0 || minutes != 0) {
+            throw new IllegalArgumentException("cannot add non-zero hours or minutes to a LocalDate");
+        }
+        return original.plusYears(years).plusMonths(months).plusDays(days);
+    }
+
+
+    // //////////////////////////////////////
+
+    public static String titleString(final DateTimeFormatter formatter, final LocalDate date) {
+        return date == null ? "" : formatter.print(date);
+    }
+
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/192b6b22/core/metamodel/src/main/java/org/apache/isis/core/progmodel/facets/value/datejodalocal/JodaLocalDateValueSemanticsProvider.java
----------------------------------------------------------------------
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/progmodel/facets/value/datejodalocal/JodaLocalDateValueSemanticsProvider.java b/core/metamodel/src/main/java/org/apache/isis/core/progmodel/facets/value/datejodalocal/JodaLocalDateValueSemanticsProvider.java
index 16ed066..27ab588 100644
--- a/core/metamodel/src/main/java/org/apache/isis/core/progmodel/facets/value/datejodalocal/JodaLocalDateValueSemanticsProvider.java
+++ b/core/metamodel/src/main/java/org/apache/isis/core/progmodel/facets/value/datejodalocal/JodaLocalDateValueSemanticsProvider.java
@@ -19,13 +19,13 @@
 
 package org.apache.isis.core.progmodel.facets.value.datejodalocal;
 
-import java.text.SimpleDateFormat;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.StringTokenizer;
 
+import com.google.common.base.Function;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 
@@ -55,18 +55,18 @@ public class JodaLocalDateValueSemanticsProvider extends ValueSemanticsProviderA
      * <p>
      * REVIEW: This seems only to have any effect if 'propertyType' is set to 'date'.
      * 
-     * @see #setPatternOverride(String)
+     * @see #setTitlePatternOverride(String)
      * @deprecated - because 'propertyType' parameter is never used
      */
     @Deprecated
     public static void setFormat(final String propertyType, final String pattern) {
-        setPatternOverride(pattern);
+        setTitlePatternOverride(pattern);
     }
     /**
      * A replacement for {@link #setFormat(String, String)}.
      */
-    public static void setPatternOverride(final String pattern) {
-        OVERRIDE_PATTERN.set(pattern);
+    public static void setTitlePatternOverride(final String pattern) {
+        OVERRIDE_TITLE_PATTERN.set(pattern);
     }
     
     /**
@@ -80,9 +80,9 @@ public class JodaLocalDateValueSemanticsProvider extends ValueSemanticsProviderA
      * 
      * <p>
      * A pre-determined list of values is available, specifically 'iso_encoding', 'iso' and 'medium' (see 
-     * {@link #NAMED_FORMATTERS}).  Alternatively,  can also specify a mask, eg <tt>dd-MMM-yyyy</tt>.
+     * {@link #NAMED_TITLE_FORMATTERS}).  Alternatively,  can also specify a mask, eg <tt>dd-MMM-yyyy</tt>.
      * 
-     * @see #NAMED_FORMATTERS  
+     * @see #NAMED_TITLE_FORMATTERS  
      */
     public final static String CFG_FORMAT_KEY = ConfigurationConstants.ROOT + "value.format.date";
     
@@ -91,20 +91,33 @@ public class JodaLocalDateValueSemanticsProvider extends ValueSemanticsProviderA
      * Keys represent the values which can be configured, and which are used for the rendering of dates.
      * 
      */
-    private static Map<String, DateTimeFormatter> NAMED_FORMATTERS = Maps.newHashMap();
+    private static Map<String, DateTimeFormatter> NAMED_TITLE_FORMATTERS = Maps.newHashMap();
     static {
-        NAMED_FORMATTERS.put("iso_encoding", DateTimeFormat.forPattern("yyyyMMdd"));
-        NAMED_FORMATTERS.put("iso", DateTimeFormat.forPattern("yyyy-MM-dd"));
-        NAMED_FORMATTERS.put("medium", DateTimeFormat.forStyle("M-"));
+        NAMED_TITLE_FORMATTERS.put("iso_encoding", DateTimeFormat.forPattern("yyyyMMdd"));
+        NAMED_TITLE_FORMATTERS.put("iso", DateTimeFormat.forPattern("yyyy-MM-dd"));
+        NAMED_TITLE_FORMATTERS.put("long", DateTimeFormat.forStyle("L-"));
+        NAMED_TITLE_FORMATTERS.put("medium", DateTimeFormat.forStyle("M-"));
+        NAMED_TITLE_FORMATTERS.put("short", DateTimeFormat.forStyle("S-"));
     }
     
-    private final static ThreadLocal<String> OVERRIDE_PATTERN = new ThreadLocal<String>() {
+    private final static ThreadLocal<String> OVERRIDE_TITLE_PATTERN = new ThreadLocal<String>() {
         @Override
         protected String initialValue() {
             return null;
         }
     };
 
+    
+    private final static List<DateTimeFormatter> PARSE_FORMATTERS = Lists.newArrayList();
+    static {
+        PARSE_FORMATTERS.add(DateTimeFormat.forStyle("L-"));
+        PARSE_FORMATTERS.add(DateTimeFormat.forStyle("M-"));
+        PARSE_FORMATTERS.add(DateTimeFormat.forStyle("S-"));
+        PARSE_FORMATTERS.add(DateTimeFormat.forPattern("yyyy-MM-dd"));
+        PARSE_FORMATTERS.add(DateTimeFormat.forPattern("yyyyMMdd"));
+    }
+    
+
 
     public static Class<? extends Facet> type() {
         return JodaLocalDateValueFacet.class;
@@ -115,8 +128,9 @@ public class JodaLocalDateValueSemanticsProvider extends ValueSemanticsProviderA
     private static final LocalDate DEFAULT_VALUE = null;
 
 
-    private final DateTimeFormatter encodingFormatter;
-    protected DateTimeFormatter titleStringFormatter;
+    private final DateTimeFormatter encodingFormatter = DateTimeFormat.forPattern("yyyyMMdd");
+    
+    private DateTimeFormatter titleStringFormatter;
     private String titleStringFormatNameOrPattern;
 
     
@@ -139,15 +153,13 @@ public class JodaLocalDateValueSemanticsProvider extends ValueSemanticsProviderA
             final FacetHolder holder, final IsisConfiguration configuration, final ValueSemanticsProviderContext context) {
         super(type(), holder, LocalDate.class, 12, Immutability.IMMUTABLE, EqualByContent.HONOURED, DEFAULT_VALUE, configuration, context);
 
-        encodingFormatter = DateTimeFormat.forPattern("yyyyMMdd");
-        
         String configuredNameOrPattern = getConfiguration().getString(CFG_FORMAT_KEY, "medium").toLowerCase().trim();
         updateTitleStringFormatter(configuredNameOrPattern);
     }
 
 
     private void updateTitleStringFormatter(String titleStringFormatNameOrPattern) {
-        titleStringFormatter = NAMED_FORMATTERS.get(titleStringFormatNameOrPattern);
+        titleStringFormatter = NAMED_TITLE_FORMATTERS.get(titleStringFormatNameOrPattern);
         if (titleStringFormatter == null) {
             titleStringFormatter = DateTimeFormat.forPattern(titleStringFormatNameOrPattern);
         }
@@ -161,24 +173,23 @@ public class JodaLocalDateValueSemanticsProvider extends ValueSemanticsProviderA
 
     @Override
     protected LocalDate doParse(final Object context, final String entry, final Localization localization) {
-        
+
         updateTitleStringFormatterIfOverridden();
+        
+        LocalDate contextDate = (LocalDate) context;
 
-        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);
+        final String dateString = entry.trim().toUpperCase();
+        if (dateString.startsWith("+") && contextDate != null) {
+            return JodaLocalDateUtil.relativeDate(contextDate, dateString, true);
+        } else if (dateString.startsWith("-")  && contextDate != null) {
+            return JodaLocalDateUtil.relativeDate(contextDate, dateString, false);
         } else {
-            return parseDate(dateString, context == null ? now() : context, localization);
+            return parseDate(dateString, contextDate, localization);
         }
     }
 
     private void updateTitleStringFormatterIfOverridden() {
-        final String overridePattern = OVERRIDE_PATTERN.get();
+        final String overridePattern = OVERRIDE_TITLE_PATTERN.get();
         if (overridePattern == null || 
             titleStringFormatNameOrPattern.equals(overridePattern)) {
             return;
@@ -188,78 +199,10 @@ public class JodaLocalDateValueSemanticsProvider extends ValueSemanticsProviderA
         updateTitleStringFormatter(overridePattern);
     }
 
-    private LocalDate parseDate(final String dateString, final Object original, final Localization localization) {
-        List<DateTimeFormatter> elements = formatsToTry(localization);
-        LocalDate parsedDate = parseDate(dateString, elements.iterator());
-        return setDate(parsedDate);
-    }
-
-    private LocalDate parseDate(final String dateString, final Iterator<DateTimeFormatter> iterator) {
-        final DateTimeFormatter format = iterator.next();
-        try {
-            return format.parseLocalDate(dateString);
-        } catch (final IllegalArgumentException e) {
-            if (iterator.hasNext()) {
-                return parseDate(dateString, iterator);
-            } else {
-                throw new TextEntryParseException("Not recognised as a date: " + dateString);
-            }
-        }
-    }
-
-    private LocalDate relativeDate(final Object object, final String str, final boolean add) {
-        if (str.equals("")) {
-            return now();
-        }
-
-        try {
-            LocalDate date = (LocalDate) 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 LocalDate relativeDate2(final LocalDate 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);
-        }
+    private LocalDate parseDate(final String dateStr, final Object original, final Localization localization) {
+        return JodaLocalDateUtil.parseDate(dateStr, localization, PARSE_FORMATTERS);
     }
+    
 
     // ///////////////////////////////////////////////////////////////////////////
     // TitleProvider
@@ -270,23 +213,20 @@ public class JodaLocalDateValueSemanticsProvider extends ValueSemanticsProviderA
         if (value == null) {
             return null;
         }
-        final LocalDate date = dateValue(value);
+        final LocalDate date = (LocalDate) value;
         DateTimeFormatter f = titleStringFormatter;
         if (localization != null) {
-            f = format(localization);
+            f = titleStringFormatter.withLocale(localization.getLocale());
         }
-        return titleString(f, date);
+        return JodaLocalDateUtil.titleString(f, date);
     }
 
     @Override
     public String titleStringWithMask(final Object value, final String usingMask) {
-        final LocalDate date = dateValue(value);
-        return titleString(new SimpleDateFormat(usingMask), date);
+        final LocalDate date = (LocalDate) value;
+        return JodaLocalDateUtil.titleString(DateTimeFormat.forPattern(usingMask), date);
     }
 
-    private String titleString(final DateTimeFormatter formatter, final LocalDate date) {
-        return date == null ? "" : formatter.print(date);
-    }
 
     // //////////////////////////////////////////////////////////////////
     // EncoderDecoder
@@ -294,7 +234,7 @@ public class JodaLocalDateValueSemanticsProvider extends ValueSemanticsProviderA
 
     @Override
     protected String doEncode(final Object object) {
-        final LocalDate date = dateValue(object);
+        final LocalDate date = (LocalDate) object;
         return encode(date);
     }
 
@@ -330,72 +270,11 @@ public class JodaLocalDateValueSemanticsProvider extends ValueSemanticsProviderA
     }
 
 
-    // //////////////////////////////////////////////////////////////////
-    // temporal-specific stuff
-    // //////////////////////////////////////////////////////////////////
-
-    protected boolean isEmpty() {
-        return false;
-    }
-
-
-
-    
-    // //////////////////////////////////////////////////////////////////
-    // temporal-specific stuff
-    // //////////////////////////////////////////////////////////////////
-
-    protected DateTimeFormatter format(final Localization localization) {
-        return DateTimeFormat.forStyle("M-").withLocale(localization.getLocale());
-    }
-
-    protected List<DateTimeFormatter> formatsToTry(Localization localization) {
-        List<DateTimeFormatter> formats = Lists.newArrayList();
-        
-        formats.add(withLocale(DateTimeFormat.forStyle("L-"), localization));
-        formats.add(withLocale(DateTimeFormat.forStyle("M-"), localization));
-        formats.add(withLocale(DateTimeFormat.forStyle("S-"), localization));
-        formats.add(withLocale(DateTimeFormat.forPattern("yyyy-MM-dd"), localization));
-        formats.add(withLocale(DateTimeFormat.forPattern("yyyyMMdd"), localization));
-
-        return formats;
-    }
-
-
-    private static DateTimeFormatter withLocale(DateTimeFormatter formatter, Localization localization) {
-        if(localization != null) {
-            Locale locale2 = localization.getLocale();
-            formatter.withLocale(locale2);
-        }
-        return formatter;
-    }
-
-    // //////////////////////////////////////
-
-    protected LocalDate add(final LocalDate original, final int years, final int months, final int days, final int hours, final int minutes) {
-        if(hours != 0 || minutes != 0) {
-            throw new IllegalArgumentException("cannot add non-zero hours or minutes to a LocalDate");
-        }
-        return original.plusYears(years).plusMonths(months).plusDays(days);
-    }
-
-    protected LocalDate now() {
-        return new LocalDate();
-    }
-
-    protected LocalDate dateValue(final Object value) {
-        return (LocalDate) value;
-    }
-
-    protected LocalDate setDate(final LocalDate date) {
-        return date;
-    }
-
     // //////////////////////////////////////
     
     @Override
     public String toString() {
-        return "DateValueSemanticsProvider: " + titleStringFormatter;
+        return "JodaLocalDateValueSemanticsProvider: " + titleStringFormatter;
     }
 
 }

http://git-wip-us.apache.org/repos/asf/isis/blob/192b6b22/core/metamodel/src/main/java/org/apache/isis/core/progmodel/facets/value/datetimejodalocal/JodaLocalDateTimeUtil.java
----------------------------------------------------------------------
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/progmodel/facets/value/datetimejodalocal/JodaLocalDateTimeUtil.java b/core/metamodel/src/main/java/org/apache/isis/core/progmodel/facets/value/datetimejodalocal/JodaLocalDateTimeUtil.java
new file mode 100644
index 0000000..7038f47
--- /dev/null
+++ b/core/metamodel/src/main/java/org/apache/isis/core/progmodel/facets/value/datetimejodalocal/JodaLocalDateTimeUtil.java
@@ -0,0 +1,124 @@
+/*
+ *  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.progmodel.facets.value.datetimejodalocal;
+
+import java.util.List;
+import java.util.StringTokenizer;
+
+import com.google.common.collect.Iterables;
+
+import org.joda.time.LocalDateTime;
+import org.joda.time.format.DateTimeFormatter;
+
+import org.apache.isis.applib.profiles.Localization;
+import org.apache.isis.core.metamodel.facets.object.parseable.TextEntryParseException;
+import org.apache.isis.core.progmodel.facets.value.JodaFunctions;
+
+public final class JodaLocalDateTimeUtil  {
+
+    private JodaLocalDateTimeUtil(){}
+    
+    static LocalDateTime parseDate(final String dateStr, final Localization localization, List<DateTimeFormatter> parseFormatters) {
+        Iterable<DateTimeFormatter> elements = Iterables.transform(parseFormatters, JodaFunctions.withLocale(localization));
+        return parseDateTime(dateStr, elements);
+    }
+
+    
+    private static LocalDateTime parseDateTime(String dateStr, Iterable<DateTimeFormatter> formatters) {
+        for(DateTimeFormatter formatter: formatters) {
+            try {
+                return formatter.parseLocalDateTime(dateStr);
+            } catch (final IllegalArgumentException e) {
+                // continue to next
+            }
+        }
+        throw new TextEntryParseException("Not recognised as a date: " + dateStr);
+    }
+
+    // //////////////////////////////////////
+
+    
+    static LocalDateTime relativeDateTime(final LocalDateTime contextDate, final String str, final boolean add) {
+        LocalDateTime relativeDate = contextDate;
+        if (str.equals("")) {
+            return contextDate;
+        }
+
+        try {
+            final StringTokenizer st = new StringTokenizer(str.substring(1), " ");
+            while (st.hasMoreTokens()) {
+                final String token = st.nextToken();
+                relativeDate = adjustDateTime(relativeDate, token, add);
+            }
+            return relativeDate;
+        } catch (final Exception e) {
+            return contextDate;
+        }
+    }
+
+    private static LocalDateTime adjustDateTime(final LocalDateTime contextDateTime, 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(contextDateTime, years, months, days, hours, minutes);
+        } else {
+            return add(contextDateTime, -years, -months, -days, -hours, -minutes);
+        }
+    }
+
+    private static LocalDateTime add(final LocalDateTime original, final int years, final int months, final int days, final int hours, final int minutes) {
+        return original.plusYears(years).plusMonths(months).plusDays(days).plusHours(hours).plusMinutes(minutes);
+    }
+
+
+    // //////////////////////////////////////
+
+    public static String titleString(final DateTimeFormatter formatter, final LocalDateTime date) {
+        return date == null ? "" : formatter.print(date);
+    }
+
+
+}

http://git-wip-us.apache.org/repos/asf/isis/blob/192b6b22/core/metamodel/src/main/java/org/apache/isis/core/progmodel/facets/value/datetimejodalocal/JodaLocalDateTimeValueSemanticsProvider.java
----------------------------------------------------------------------
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/progmodel/facets/value/datetimejodalocal/JodaLocalDateTimeValueSemanticsProvider.java b/core/metamodel/src/main/java/org/apache/isis/core/progmodel/facets/value/datetimejodalocal/JodaLocalDateTimeValueSemanticsProvider.java
index a7b9165..d3f1cee 100644
--- a/core/metamodel/src/main/java/org/apache/isis/core/progmodel/facets/value/datetimejodalocal/JodaLocalDateTimeValueSemanticsProvider.java
+++ b/core/metamodel/src/main/java/org/apache/isis/core/progmodel/facets/value/datetimejodalocal/JodaLocalDateTimeValueSemanticsProvider.java
@@ -19,22 +19,121 @@
 
 package org.apache.isis.core.progmodel.facets.value.datetimejodalocal;
 
-import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
 
 import org.joda.time.LocalDateTime;
+import org.joda.time.format.DateTimeFormat;
+import org.joda.time.format.DateTimeFormatter;
+import org.joda.time.format.ISODateTimeFormat;
 
 import org.apache.isis.applib.adapters.EncoderDecoder;
+import org.apache.isis.applib.adapters.EncodingException;
 import org.apache.isis.applib.adapters.Parser;
+import org.apache.isis.applib.profiles.Localization;
+import org.apache.isis.core.commons.config.ConfigurationConstants;
 import org.apache.isis.core.commons.config.IsisConfiguration;
+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.progmodel.facets.object.value.ValueSemanticsProviderAndFacetAbstract;
 import org.apache.isis.core.progmodel.facets.object.value.ValueSemanticsProviderContext;
 
-public class JodaLocalDateTimeValueSemanticsProvider extends JodaLocalDateTimeValueSemanticsProviderAbstract<LocalDateTime> {
+public class JodaLocalDateTimeValueSemanticsProvider extends ValueSemanticsProviderAndFacetAbstract<LocalDateTime> implements JodaLocalDateTimeValueFacet {
+
+    
+    /**
+     * Introduced to allow BDD tests to provide a different format string "mid-flight".
+     * 
+     * <p>
+     * REVIEW: This seems only to have any effect if 'propertyType' is set to 'date'.
+     * 
+     * @see #setTitlePatternOverride(String)
+     * @deprecated - because 'propertyType' parameter is never used
+     */
+    @Deprecated
+    public static void setFormat(final String propertyType, final String pattern) {
+        setTitlePatternOverride(pattern);
+    }
+    /**
+     * A replacement for {@link #setFormat(String, String)}.
+     */
+    public static void setTitlePatternOverride(final String pattern) {
+        OVERRIDE_TITLE_PATTERN.set(pattern);
+    }
+    
+    /**
+     * Key to indicate how LocalDateTime should be parsed/rendered.
+     * 
+     * <p>
+     * eg:
+     * <pre>
+     * isis.value.format.datetime=iso
+     * </pre>
+     * 
+     * <p>
+     * A pre-determined list of values is available, specifically 'iso_encoding', 'iso' and 'medium' (see 
+     * {@link #NAMED_TITLE_FORMATTERS}).  Alternatively,  can also specify a mask, eg <tt>dd-MMM-yyyy</tt>.
+     * 
+     * @see #NAMED_TITLE_FORMATTERS  
+     */
+    public final static String CFG_FORMAT_KEY = ConfigurationConstants.ROOT + "value.format.datetime";
+    
+    
+    /**
+     * Keys represent the values which can be configured, and which are used for the rendering of dates.
+     * 
+     */
+    private static Map<String, DateTimeFormatter> NAMED_TITLE_FORMATTERS = Maps.newHashMap();
+    static {
+        NAMED_TITLE_FORMATTERS.put("iso_encoding", ISODateTimeFormat.basicDateTime());
+        NAMED_TITLE_FORMATTERS.put("iso", ISODateTimeFormat.basicDateTimeNoMillis());
+        NAMED_TITLE_FORMATTERS.put("long", DateTimeFormat.forStyle("LL"));
+        NAMED_TITLE_FORMATTERS.put("medium", DateTimeFormat.forStyle("MM"));
+        NAMED_TITLE_FORMATTERS.put("short", DateTimeFormat.forStyle("SS"));
+    }
+    
+    private final static ThreadLocal<String> OVERRIDE_TITLE_PATTERN = new ThreadLocal<String>() {
+        @Override
+        protected String initialValue() {
+            return null;
+        }
+    };
+
+    
+    private final static List<DateTimeFormatter> PARSE_FORMATTERS = Lists.newArrayList();
+    static {
+        PARSE_FORMATTERS.add(DateTimeFormat.forStyle("LL"));
+        PARSE_FORMATTERS.add(DateTimeFormat.forStyle("MM"));
+        PARSE_FORMATTERS.add(DateTimeFormat.forStyle("SS"));
+        PARSE_FORMATTERS.add(ISODateTimeFormat.basicDateTimeNoMillis());
+        PARSE_FORMATTERS.add(ISODateTimeFormat.basicDateTime());
+    }
+    
+
+
+    public static Class<? extends Facet> type() {
+        return JodaLocalDateTimeValueFacet.class;
+    }
+
 
     // no default
     private static final LocalDateTime DEFAULT_VALUE = null;
 
 
+    private final DateTimeFormatter encodingFormatter = ISODateTimeFormat.basicDateTime();
+    
+    private DateTimeFormatter titleStringFormatter;
+    private String titleStringFormatNameOrPattern;
+
+    
+    // //////////////////////////////////////
+    // constructor
+    // //////////////////////////////////////
+
     /**
      * Required because implementation of {@link Parser} and
      * {@link EncoderDecoder}.
@@ -43,30 +142,135 @@ public class JodaLocalDateTimeValueSemanticsProvider extends JodaLocalDateTimeVa
         this(null, null, null);
     }
 
-    public JodaLocalDateTimeValueSemanticsProvider(final FacetHolder holder, final IsisConfiguration configuration, final ValueSemanticsProviderContext context) {
-        super(holder, LocalDateTime.class, DEFAULT_VALUE, configuration, context);
+    /**
+     * Uses {@link #type()} as the facet type.
+     */
+    public JodaLocalDateTimeValueSemanticsProvider(
+            final FacetHolder holder, final IsisConfiguration configuration, final ValueSemanticsProviderContext context) {
+        super(type(), holder, LocalDateTime.class, 12, Immutability.IMMUTABLE, EqualByContent.HONOURED, DEFAULT_VALUE, configuration, context);
+
+        String configuredNameOrPattern = getConfiguration().getString(CFG_FORMAT_KEY, "medium").toLowerCase().trim();
+        updateTitleStringFormatter(configuredNameOrPattern);
     }
 
+
+    private void updateTitleStringFormatter(String titleStringFormatNameOrPattern) {
+        titleStringFormatter = NAMED_TITLE_FORMATTERS.get(titleStringFormatNameOrPattern);
+        if (titleStringFormatter == null) {
+            titleStringFormatter = DateTimeFormat.forPattern(titleStringFormatNameOrPattern);
+        }
+        this.titleStringFormatNameOrPattern = titleStringFormatNameOrPattern; 
+    }
+    
+
+    // //////////////////////////////////////////////////////////////////
+    // Parsing
+    // //////////////////////////////////////////////////////////////////
+
     @Override
-    protected LocalDateTime add(final LocalDateTime original, final int years, final int months, final int days, final int hours, final int minutes) {
-        if(hours != 0 || minutes != 0) {
-            throw new IllegalArgumentException("cannot add non-zero hours or minutes to a LocalDateTime");
+    protected LocalDateTime doParse(final Object context, final String entry, final Localization localization) {
+
+        updateTitleStringFormatterIfOverridden();
+        
+        LocalDateTime contextDateTime = (LocalDateTime) context;
+
+        final String dateString = entry.trim().toUpperCase();
+        if (dateString.startsWith("+") && contextDateTime != null) {
+            return JodaLocalDateTimeUtil.relativeDateTime(contextDateTime, dateString, true);
+        } else if (dateString.startsWith("-")  && contextDateTime != null) {
+            return JodaLocalDateTimeUtil.relativeDateTime(contextDateTime, dateString, false);
+        } else {
+            return parseDateTime(dateString, contextDateTime, localization);
+        }
+    }
+
+    private void updateTitleStringFormatterIfOverridden() {
+        final String overridePattern = OVERRIDE_TITLE_PATTERN.get();
+        if (overridePattern == null || 
+            titleStringFormatNameOrPattern.equals(overridePattern)) {
+            return;
+        } 
+        
+        // (re)create format
+        updateTitleStringFormatter(overridePattern);
+    }
+
+    private LocalDateTime parseDateTime(final String dateStr, final Object original, final Localization localization) {
+        return JodaLocalDateTimeUtil.parseDate(dateStr, localization, PARSE_FORMATTERS);
+    }
+    
+
+    // ///////////////////////////////////////////////////////////////////////////
+    // TitleProvider
+    // ///////////////////////////////////////////////////////////////////////////
+
+    @Override
+    public String titleString(final Object value, final Localization localization) {
+        if (value == null) {
+            return null;
+        }
+        final LocalDateTime dateTime = (LocalDateTime) value;
+        DateTimeFormatter f = titleStringFormatter;
+        if (localization != null) {
+            f = titleStringFormatter.withLocale(localization.getLocale());
+        }
+        return JodaLocalDateTimeUtil.titleString(f, dateTime);
+    }
+
+    @Override
+    public String titleStringWithMask(final Object value, final String usingMask) {
+        final LocalDateTime dateTime = (LocalDateTime) value;
+        return JodaLocalDateTimeUtil.titleString(DateTimeFormat.forPattern(usingMask), dateTime);
+    }
+
+
+    // //////////////////////////////////////////////////////////////////
+    // EncoderDecoder
+    // //////////////////////////////////////////////////////////////////
+
+    @Override
+    protected String doEncode(final Object object) {
+        final LocalDateTime date = (LocalDateTime) object;
+        return encode(date);
+    }
+
+    private synchronized String encode(final LocalDateTime date) {
+        return encodingFormatter.print(date);
+    }
+
+    @Override
+    protected LocalDateTime doRestore(final String data) {
+        try {
+            return parse(data);
+        } catch (final IllegalArgumentException e) {
+            throw new EncodingException(e);
         }
-        return original.plusYears(years).plusMonths(months).plusDays(days);
     }
 
+    private synchronized LocalDateTime parse(final String data) {
+        return encodingFormatter.parseLocalDateTime(data);
+    }
+
+    // //////////////////////////////////////////////////////////////////
+    // JodaLocalDateValueFacet
+    // //////////////////////////////////////////////////////////////////
+
     @Override
-    protected LocalDateTime now() {
-        return new LocalDateTime();
+    public final LocalDateTime dateValue(final ObjectAdapter object) {
+        return (LocalDateTime) (object == null ? null : object.getObject());
     }
 
     @Override
-    protected Date dateValue(final Object value) {
-        return ((LocalDateTime) value).toDateTime().toDate();
+    public final ObjectAdapter createValue(final LocalDateTime date) {
+        return getAdapterManager().adapterFor(date);
     }
 
+
+    // //////////////////////////////////////
+    
     @Override
-    protected LocalDateTime setDate(final Date date) {
-        return new LocalDateTime(date.getTime());
+    public String toString() {
+        return "JodaLocalDateValueSemanticsProvider: " + titleStringFormatter;
     }
+
 }

http://git-wip-us.apache.org/repos/asf/isis/blob/192b6b22/core/metamodel/src/main/java/org/apache/isis/core/progmodel/facets/value/datetimejodalocal/JodaLocalDateTimeValueSemanticsProviderAbstract.java
----------------------------------------------------------------------
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/progmodel/facets/value/datetimejodalocal/JodaLocalDateTimeValueSemanticsProviderAbstract.java b/core/metamodel/src/main/java/org/apache/isis/core/progmodel/facets/value/datetimejodalocal/JodaLocalDateTimeValueSemanticsProviderAbstract.java
deleted file mode 100644
index dc94786..0000000
--- a/core/metamodel/src/main/java/org/apache/isis/core/progmodel/facets/value/datetimejodalocal/JodaLocalDateTimeValueSemanticsProviderAbstract.java
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- *  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.progmodel.facets.value.datetimejodalocal;
-
-import java.text.DateFormat;
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-
-import com.google.common.collect.Maps;
-
-import org.apache.isis.applib.profiles.Localization;
-import org.apache.isis.core.commons.config.ConfigurationConstants;
-import org.apache.isis.core.commons.config.IsisConfiguration;
-import org.apache.isis.core.metamodel.facetapi.FacetHolder;
-import org.apache.isis.core.progmodel.facets.object.value.ValueSemanticsProviderContext;
-import org.apache.isis.core.progmodel.facets.value.ValueSemanticsProviderAbstractTemporal;
-
-public abstract class JodaLocalDateTimeValueSemanticsProviderAbstract<T> extends ValueSemanticsProviderAbstractTemporal<T> {
-
-    private static Map<String, DateFormat> formats = Maps.newHashMap();
-
-    static {
-        formats.put(ISO_ENCODING_FORMAT, createDateEncodingFormat("yyyyMMdd"));
-        formats.put("iso", createDateFormat("yyyy-MM-dd"));
-        formats.put("medium", DateFormat.getDateInstance(DateFormat.MEDIUM));
-    }
-
-    public JodaLocalDateTimeValueSemanticsProviderAbstract(final FacetHolder holder, final Class<T> adaptedClass, final T defaultValue, final IsisConfiguration configuration, final ValueSemanticsProviderContext context) {
-        super("date", holder, adaptedClass, 12, Immutability.IMMUTABLE, EqualByContent.HONOURED, defaultValue, configuration, context);
-
-        final String formatRequired = configuration.getString(ConfigurationConstants.ROOT + "value.format.date");
-        if (formatRequired == null) {
-            format = formats().get(defaultFormat());
-        } else {
-            setMask(formatRequired);
-        }
-    }
-
-
-    // //////////////////////////////////////////////////////////////////
-    // temporal-specific stuff
-    // //////////////////////////////////////////////////////////////////
-
-    @Override
-    protected void clearFields(final Calendar cal) {
-        cal.set(Calendar.HOUR, 0);
-        cal.set(Calendar.HOUR_OF_DAY, 0);
-        cal.set(Calendar.MINUTE, 0);
-        cal.set(Calendar.SECOND, 0);
-        cal.set(Calendar.AM_PM, 0);
-        cal.set(Calendar.MILLISECOND, 0);
-    }
-
-    @Override
-    protected String defaultFormat() {
-        return "medium";
-    }
-
-    @Override
-    protected boolean ignoreTimeZone() {
-        return true;
-    }
-
-    @Override
-    protected Map<String, DateFormat> formats() {
-        return formats;
-    }
-
-    @Override
-    public String toString() {
-        return "DateValueSemanticsProvider: " + format;
-    }
-
-    @Override
-    protected DateFormat format(final Localization localization) {
-        final DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM, localization.getLocale());
-        dateFormat.setTimeZone(UTC_TIME_ZONE);
-        return dateFormat;
-    }
-
-    protected List<DateFormat> formatsToTry(Localization localization) {
-        List<DateFormat> formats = new ArrayList<DateFormat>();
-
-        Locale locale = localization == null ? Locale.getDefault() : localization.getLocale();
-        formats.add(DateFormat.getDateInstance(DateFormat.LONG, locale));
-        formats.add(DateFormat.getDateInstance(DateFormat.MEDIUM, locale));
-        formats.add(DateFormat.getDateInstance(DateFormat.SHORT, locale));
-        formats.add(createDateFormat("yyyy-MM-dd"));
-        formats.add(createDateFormat("yyyyMMdd"));
-
-        for (DateFormat format : formats) {
-            format.setTimeZone(UTC_TIME_ZONE);
-        }
-
-        return formats;
-    }
-
-}