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 }