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.datetimejodalocal;
021
022import java.util.List;
023import java.util.Map;
024
025import com.google.common.collect.Lists;
026import com.google.common.collect.Maps;
027
028import org.joda.time.LocalDateTime;
029import org.joda.time.format.DateTimeFormat;
030import org.joda.time.format.DateTimeFormatter;
031import org.joda.time.format.ISODateTimeFormat;
032
033import org.apache.isis.applib.adapters.EncoderDecoder;
034import org.apache.isis.applib.adapters.EncodingException;
035import org.apache.isis.applib.adapters.Parser;
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.progmodel.facets.object.value.ValueSemanticsProviderAndFacetAbstract;
043import org.apache.isis.core.progmodel.facets.object.value.ValueSemanticsProviderContext;
044
045public class JodaLocalDateTimeValueSemanticsProvider extends ValueSemanticsProviderAndFacetAbstract<LocalDateTime> implements JodaLocalDateTimeValueFacet {
046
047    
048    /**
049     * Introduced to allow BDD tests to provide a different format string "mid-flight".
050     * 
051     * <p>
052     * REVIEW: This seems only to have any effect if 'propertyType' is set to 'date'.
053     * 
054     * @see #setTitlePatternOverride(String)
055     * @deprecated - because 'propertyType' parameter is never used
056     */
057    @Deprecated
058    public static void setFormat(final String propertyType, final String pattern) {
059        setTitlePatternOverride(pattern);
060    }
061    /**
062     * A replacement for {@link #setFormat(String, String)}.
063     */
064    public static void setTitlePatternOverride(final String pattern) {
065        OVERRIDE_TITLE_PATTERN.set(pattern);
066    }
067    
068    /**
069     * Key to indicate how LocalDateTime should be parsed/rendered.
070     * 
071     * <p>
072     * eg:
073     * <pre>
074     * isis.value.format.datetime=iso
075     * </pre>
076     * 
077     * <p>
078     * A pre-determined list of values is available, specifically 'iso_encoding', 'iso' and 'medium' (see 
079     * {@link #NAMED_TITLE_FORMATTERS}).  Alternatively,  can also specify a mask, eg <tt>dd-MMM-yyyy</tt>.
080     * 
081     * @see #NAMED_TITLE_FORMATTERS  
082     */
083    public final static String CFG_FORMAT_KEY = ConfigurationConstants.ROOT + "value.format.datetime";
084    
085    
086    /**
087     * Keys represent the values which can be configured, and which are used for the rendering of dates.
088     * 
089     */
090    private static Map<String, DateTimeFormatter> NAMED_TITLE_FORMATTERS = Maps.newHashMap();
091    static {
092        NAMED_TITLE_FORMATTERS.put("iso_encoding", ISODateTimeFormat.basicDateTime());
093        NAMED_TITLE_FORMATTERS.put("iso", ISODateTimeFormat.basicDateTimeNoMillis());
094        NAMED_TITLE_FORMATTERS.put("long", DateTimeFormat.forStyle("LL"));
095        NAMED_TITLE_FORMATTERS.put("medium", DateTimeFormat.forStyle("MM"));
096        NAMED_TITLE_FORMATTERS.put("short", DateTimeFormat.forStyle("SS"));
097    }
098    
099    private final static ThreadLocal<String> OVERRIDE_TITLE_PATTERN = new ThreadLocal<String>() {
100        @Override
101        protected String initialValue() {
102            return null;
103        }
104    };
105
106    
107    private final static List<DateTimeFormatter> PARSE_FORMATTERS = Lists.newArrayList();
108    static {
109        PARSE_FORMATTERS.add(DateTimeFormat.forStyle("LL"));
110        PARSE_FORMATTERS.add(DateTimeFormat.forStyle("MM"));
111        PARSE_FORMATTERS.add(DateTimeFormat.forStyle("SS"));
112        PARSE_FORMATTERS.add(ISODateTimeFormat.basicDateTimeNoMillis());
113        PARSE_FORMATTERS.add(ISODateTimeFormat.basicDateTime());
114    }
115    
116
117
118    public static Class<? extends Facet> type() {
119        return JodaLocalDateTimeValueFacet.class;
120    }
121
122
123    // no default
124    private static final LocalDateTime DEFAULT_VALUE = null;
125
126
127    private final DateTimeFormatter encodingFormatter = ISODateTimeFormat.basicDateTime();
128    
129    private DateTimeFormatter titleStringFormatter;
130    private String titleStringFormatNameOrPattern;
131
132    
133    // //////////////////////////////////////
134    // constructor
135    // //////////////////////////////////////
136
137    /**
138     * Required because implementation of {@link Parser} and
139     * {@link EncoderDecoder}.
140     */
141    public JodaLocalDateTimeValueSemanticsProvider() {
142        this(null, null, null);
143    }
144
145    /**
146     * Uses {@link #type()} as the facet type.
147     */
148    public JodaLocalDateTimeValueSemanticsProvider(
149            final FacetHolder holder, final IsisConfiguration configuration, final ValueSemanticsProviderContext context) {
150        super(type(), holder, LocalDateTime.class, 12, Immutability.IMMUTABLE, EqualByContent.HONOURED, DEFAULT_VALUE, configuration, context);
151
152        String configuredNameOrPattern = getConfiguration().getString(CFG_FORMAT_KEY, "medium").toLowerCase().trim();
153        updateTitleStringFormatter(configuredNameOrPattern);
154    }
155
156
157    private void updateTitleStringFormatter(String titleStringFormatNameOrPattern) {
158        titleStringFormatter = NAMED_TITLE_FORMATTERS.get(titleStringFormatNameOrPattern);
159        if (titleStringFormatter == null) {
160            titleStringFormatter = DateTimeFormat.forPattern(titleStringFormatNameOrPattern);
161        }
162        this.titleStringFormatNameOrPattern = titleStringFormatNameOrPattern; 
163    }
164    
165
166    // //////////////////////////////////////////////////////////////////
167    // Parsing
168    // //////////////////////////////////////////////////////////////////
169
170    @Override
171    protected LocalDateTime doParse(final Object context, final String entry, final Localization localization) {
172
173        updateTitleStringFormatterIfOverridden();
174        
175        LocalDateTime contextDateTime = (LocalDateTime) context;
176
177        final String dateString = entry.trim().toUpperCase();
178        if (dateString.startsWith("+") && contextDateTime != null) {
179            return JodaLocalDateTimeUtil.relativeDateTime(contextDateTime, dateString, true);
180        } else if (dateString.startsWith("-")  && contextDateTime != null) {
181            return JodaLocalDateTimeUtil.relativeDateTime(contextDateTime, dateString, false);
182        } else {
183            return parseDateTime(dateString, contextDateTime, localization);
184        }
185    }
186
187    private void updateTitleStringFormatterIfOverridden() {
188        final String overridePattern = OVERRIDE_TITLE_PATTERN.get();
189        if (overridePattern == null || 
190            titleStringFormatNameOrPattern.equals(overridePattern)) {
191            return;
192        } 
193        
194        // (re)create format
195        updateTitleStringFormatter(overridePattern);
196    }
197
198    private LocalDateTime parseDateTime(final String dateStr, final Object original, final Localization localization) {
199        return JodaLocalDateTimeUtil.parseDate(dateStr, localization, PARSE_FORMATTERS);
200    }
201    
202
203    // ///////////////////////////////////////////////////////////////////////////
204    // TitleProvider
205    // ///////////////////////////////////////////////////////////////////////////
206
207    @Override
208    public String titleString(final Object value, final Localization localization) {
209        if (value == null) {
210            return null;
211        }
212        final LocalDateTime dateTime = (LocalDateTime) value;
213        DateTimeFormatter f = titleStringFormatter;
214        if (localization != null) {
215            f = titleStringFormatter.withLocale(localization.getLocale());
216        }
217        return JodaLocalDateTimeUtil.titleString(f, dateTime);
218    }
219
220    @Override
221    public String titleStringWithMask(final Object value, final String usingMask) {
222        final LocalDateTime dateTime = (LocalDateTime) value;
223        return JodaLocalDateTimeUtil.titleString(DateTimeFormat.forPattern(usingMask), dateTime);
224    }
225
226
227    // //////////////////////////////////////////////////////////////////
228    // EncoderDecoder
229    // //////////////////////////////////////////////////////////////////
230
231    @Override
232    protected String doEncode(final Object object) {
233        final LocalDateTime date = (LocalDateTime) object;
234        return encode(date);
235    }
236
237    private synchronized String encode(final LocalDateTime date) {
238        return encodingFormatter.print(date);
239    }
240
241    @Override
242    protected LocalDateTime doRestore(final String data) {
243        try {
244            return parse(data);
245        } catch (final IllegalArgumentException e) {
246            throw new EncodingException(e);
247        }
248    }
249
250    private synchronized LocalDateTime parse(final String data) {
251        return encodingFormatter.parseLocalDateTime(data);
252    }
253
254    // //////////////////////////////////////////////////////////////////
255    // JodaLocalDateValueFacet
256    // //////////////////////////////////////////////////////////////////
257
258    @Override
259    public final LocalDateTime dateValue(final ObjectAdapter object) {
260        return (LocalDateTime) (object == null ? null : object.getObject());
261    }
262
263    @Override
264    public final ObjectAdapter createValue(final LocalDateTime date) {
265        return getAdapterManager().adapterFor(date);
266    }
267
268
269    // //////////////////////////////////////
270    
271    @Override
272    public String toString() {
273        return "JodaLocalDateValueSemanticsProvider: " + titleStringFormatter;
274    }
275
276}