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 }