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