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