001 package hirondelle.web4j.ui.tag;
002
003 import hirondelle.web4j.BuildImpl;
004 import hirondelle.web4j.util.TimeSource;
005 import hirondelle.web4j.request.DateConverter;
006 import hirondelle.web4j.ui.translate.Translator;
007 import hirondelle.web4j.util.Util;
008 import hirondelle.web4j.model.DateTime;
009 import static hirondelle.web4j.util.Consts.NOT_FOUND;
010 import static hirondelle.web4j.util.Consts.EMPTY_STRING;
011
012 import java.util.*;
013 import java.util.logging.Logger;
014
015 /**
016 Custom tag to display a {@link DateTime} in a particular format.
017
018 <P>This class uses:
019 <ul>
020 <li>{@link hirondelle.web4j.request.LocaleSource} to determine the Locale associated with the current request
021 <li>{@link DateConverter} to format the given date
022 <li>{@link Translator} for localizing the argument passed to {@link #setPatternKey}.
023 </ul>
024
025 <h3>Examples</h3>
026 <P>Display the current system date, with the default format defined by {@link DateConverter} :
027 <PRE>{@code
028 <w:showDateTime/>
029 }</PRE>
030
031 <P>Display a specific date object, present in any scope :
032 <PRE><w:showDateTime <a href="#setName(java.lang.String)">name</a>="dateOfBirth"/></PRE>
033
034 <P>Display a date returned by some object in scope :
035 <PRE>{@code
036 <c:set value="${visit.lunchDate}" var="lunchDate"/>
037 <w:showDateTime name="lunchDate"/>
038 }</PRE>
039
040 <P>Display with a non-default date format :
041 <PRE><w:showDateTime name="lunchDate" <a href="#setPattern(java.lang.String)">pattern</a>="YYYY-MM-DD"/></PRE>
042
043 <P>Display with a non-default date format sensitive to {@link Locale} :
044 <PRE><w:showDateTime name="lunchDate" <a href="#setPatternKey(java.lang.String)">patternKey</a>="next.visit.lunch.date"/></PRE>
045
046 <P>Suppress the display of midnight, using a pipe-separated list of 'midnights' :
047 <PRE><w:showDateTime name="lunchDate" <a href="#setSuppressMidnight(java.lang.String)">suppressMidnight</a>="12:00 AM|00 h 00"/></PRE>
048 */
049 public final class ShowDateTime extends TagHelper {
050
051 /**
052 Optionally set the name of a {@link DateTime} object already present in some scope.
053 Searches from narrow to wide scope to find the corresponding object.
054
055 <P>If this method is called and no corresponding object can be found using the
056 given name, then this tag will emit an empty String.
057
058 <P>If this method is not called at all, then the current system date is used, as
059 defined by the configured {@link TimeSource}.
060
061 @param aName must have content.
062 */
063 public void setName(String aName){
064 checkForContent("Name", aName);
065 Object object = getPageContext().findAttribute(aName);
066 if ( object == null ) {
067 handleErrorCondition("Cannot find object named " + Util.quote(aName) + " in any scope.");
068 }
069 else {
070 if (object instanceof DateTime){
071 fTarget = Target.OBJECT_DATE_TIME;
072 fDateTime = (DateTime)object;
073 }
074 else {
075 handleErrorCondition(
076 "Object named " + Util.quote(aName) + " is not a hirondelle.web4j.model.DateTime. It is a " +
077 object.getClass().getName()
078 );
079 }
080 }
081 }
082
083 /**
084 Optionally set the format for rendering the date.
085
086 <P>Setting this attribute will override the default format used by
087 {@link DateConverter}.
088
089 <P><span class="highlight">Calling this method is suitable only when
090 the date format does not depend on {@link Locale}.</span> Otherwise,
091 {@link #setPatternKey(String)} must be used instead.
092
093 <P>Only one of {@link #setPattern(String)} and {@link #setPatternKey(String)}
094 can be called at a time.
095
096 @param aFormat has content, and is a date format suitable for the <tt>format</tt> .
097 methods of {@link DateTime}.
098 */
099 public void setPattern(String aFormat){
100 checkForContent("Pattern", aFormat);
101 fFormat = aFormat;
102 }
103
104 /**
105 Optionally set the format for rendering the date according to {@link Locale}.
106
107 <P>Setting this attribute will override the default format used by
108 {@link DateConverter}.
109
110 <P>This method uses a {@link Translator} to look up the "real"
111 date pattern to be used, according to the {@link Locale} returned
112 by {@link hirondelle.web4j.request.LocaleSource}.
113
114 <P>For example, if the value '<tt>format.next.lunch.date</tt>' is passed to
115 this method, then that value is passed to a {@link Translator}, which will return
116 a pattern specific to the {@link Locale} attached to this request, such as
117 '<tt>EEE, dd MMM</tt>' (for a <tt>Date</tt>) or <tt>YYYY-MM-DD</tt> (for a {@link DateTime}).
118
119 <P>Only one of {@link #setPattern(String)} and {@link #setPatternKey(String)}
120 can be called at a time.
121
122 @param aFormatKey has content, and, when passed to {@link Translator}, will
123 return a date format suitable for the <tt>format</tt> methods of {@link DateTime}.
124 */
125 public void setPatternKey(String aFormatKey){
126 checkForContent("PatternKey", aFormatKey);
127 fFormatKey = aFormatKey;
128 }
129
130 /**
131 Optionally suppress the display of midnight.
132
133 <P>For example, set this attribute to '<tt>00:00:00</tt>' to force '<tt>1999-12-31 00:00:00</tt>' to display as
134 <tt>1999-12-31</tt>, without the time.
135
136 <P>If this attribute is set, and if any of the <tt>aMidnightStyles</tt> is found <em>anywhere</em> in the formatted date,
137 then the formatted date is truncated, starting from the given midnight style. That is, all text appearing after
138 the midnight style is removed, including any time zone information. (Then the result is trimmed.)
139
140 @param aMidnightStyles is pipe-separated list of Strings which denote the possible forms of
141 midnight. Example value : '00:00|00 h 00'.
142 */
143 public void setSuppressMidnight(String aMidnightStyles){
144 StringTokenizer parser = new StringTokenizer(aMidnightStyles, "|");
145 while ( parser.hasMoreElements() ){
146 fMidnightStyles = new ArrayList<String>();
147 String midnightStyle = (String)parser.nextElement();
148 if( Util.textHasContent(midnightStyle)){
149 fMidnightStyles.add(midnightStyle.trim());
150 }
151 }
152 fLogger.fine("Midnight styles: " + fMidnightStyles);
153 }
154
155 protected void crossCheckAttributes() {
156 if(fFormatKey != null && fFormat != null){
157 handleErrorCondition("Cannot specify both 'pattern' and 'patternKey' attributes at the same time.");
158 }
159 }
160
161 @Override protected String getEmittedText(String aOriginalBody) {
162 String result = EMPTY_STRING;
163 if( fFoundError ) return result;
164
165 if(Target.CURRENT_DATE_TIME == fTarget){
166 fDateTime = DateTime.now(BuildImpl.forTimeZoneSource().get(getRequest()));
167 }
168 result = formatDateTime();
169
170 if( hasMidnightStyles() ) {
171 result = removeMidnightIfPresent(result);
172 }
173 return result;
174 }
175
176 // PRIVATE
177 private DateTime fDateTime; //defaults to now; cannot init here, since request does not exist yet
178
179 /** The item to be formatted. */
180 private enum Target {
181 /** If no object is specified at all, then the current date time is assumed. */
182 CURRENT_DATE_TIME,
183 OBJECT_DATE_TIME,
184 }
185 private Target fTarget = Target.CURRENT_DATE_TIME; //default
186
187 private String fFormat;
188 private String fFormatKey;
189 private List<String> fMidnightStyles = new ArrayList<String>();
190
191 /** Flags presence of error conditions. If true, then only an empty String is emitted. */
192 private boolean fFoundError;
193
194 private static final Logger fLogger = Util.getLogger(ShowDate.class);
195
196 private String formatDateTime(){
197 String result = "";
198 Locale locale = getLocale();
199 if(fFormat == null && fFormatKey == null){
200 DateConverter dateConverter = BuildImpl.forDateConverter();
201 result = dateConverter.formatEyeFriendlyDateTime(fDateTime, locale);
202 }
203 else if(fFormat != null && fFormatKey == null){
204 result = fDateTime.format(fFormat, getLocale());
205 }
206 else if(fFormat == null && fFormatKey != null){
207 Translator translator = BuildImpl.forTranslator();
208 String localPattern = translator.get(fFormatKey, locale);
209 result = fDateTime.format(localPattern, locale);
210 }
211 return result;
212 }
213
214 private Locale getLocale(){
215 return BuildImpl.forLocaleSource().get(getRequest());
216 }
217
218 private boolean hasMidnightStyles(){
219 return ! fMidnightStyles.isEmpty();
220 }
221
222 private String removeMidnightIfPresent(String aFormattedDate){
223 String result = aFormattedDate;
224 for(String midnightStyle : fMidnightStyles){
225 if ( hasMidnight(aFormattedDate, midnightStyle) ){
226 result = removeMidnight(aFormattedDate, midnightStyle);
227 }
228 }
229 return result.trim();
230 }
231
232 private boolean hasMidnight(String aFormattedDate, String aMidnightStyle){
233 return aFormattedDate.indexOf(aMidnightStyle) != NOT_FOUND;
234 }
235
236 private String removeMidnight(String aFormattedDate, String aMidnightStyle){
237 int midnight = aFormattedDate.indexOf(aMidnightStyle);
238 return aFormattedDate.substring(0,midnight);
239 }
240
241 private void handleErrorCondition(String aMessage){
242 fFoundError = true;
243 String message = aMessage + " Page Name : " + getPageName();
244 fLogger.severe(message);
245 }
246 }