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>&lt;w:showDate <a href="#setName(java.lang.String)">name</a>="dateOfBirth"/&gt;</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>&lt;w:showDate name="lunchDate" <a href="#setPattern(java.lang.String)">pattern</a>="E, MMM dd"/&gt;</PRE>
045     
046     <P>Display with a non-default date format sensitive to {@link Locale} :
047    <PRE>&lt;w:showDate name="lunchDate" <a href="#setPatternKey(java.lang.String)">patternKey</a>="next.visit.lunch.date"/&gt;</PRE>
048     
049     <P>Display in a specific time zone :
050    <PRE>&lt;w:showDate name="lunchDate" <a href="#setTimeZone(java.lang.String)">timeZone</a>="America/Montreal"/&gt;</PRE>
051     
052     <P>Suppress the display of midnight, using a pipe-separated list of 'midnights' :
053    <PRE>&lt;w:showDate name="lunchDate" <a href="#setSuppressMidnight(java.lang.String)">suppressMidnight</a>="12:00 AM|00 h 00"/&gt;</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    }