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>&lt;w:showDateTime <a href="#setName(java.lang.String)">name</a>="dateOfBirth"/&gt;</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>&lt;w:showDateTime name="lunchDate" <a href="#setPattern(java.lang.String)">pattern</a>="YYYY-MM-DD"/&gt;</PRE>
042     
043     <P>Display with a non-default date format sensitive to {@link Locale} :
044    <PRE>&lt;w:showDateTime name="lunchDate" <a href="#setPatternKey(java.lang.String)">patternKey</a>="next.visit.lunch.date"/&gt;</PRE>
045     
046     <P>Suppress the display of midnight, using a pipe-separated list of 'midnights' :
047    <PRE>&lt;w:showDateTime name="lunchDate" <a href="#setSuppressMidnight(java.lang.String)">suppressMidnight</a>="12:00 AM|00 h 00"/&gt;</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    }