001    package hirondelle.web4j.request;
002    
003    import hirondelle.web4j.BuildImpl;
004    import hirondelle.web4j.model.Code;
005    import hirondelle.web4j.model.DateTime;
006    import hirondelle.web4j.model.Decimal;
007    import hirondelle.web4j.model.Id;
008    import hirondelle.web4j.readconfig.Config;
009    import hirondelle.web4j.security.SafeText;
010    import hirondelle.web4j.util.Consts;
011    import hirondelle.web4j.util.Util;
012    
013    import java.math.BigDecimal;
014    import java.text.DecimalFormat;
015    import java.text.NumberFormat;
016    import java.util.Date;
017    import java.util.Locale;
018    import java.util.TimeZone;
019    import java.util.regex.Pattern;
020    
021    /**
022     Standard display formats for the application.
023     
024     <P>The formats used by this class are <em>mostly</em> configured in 
025     <tt>web.xml</tt>, and are read by this class upon startup. 
026     <span class="highlight">See the <a href='http://www.web4j.com/UserGuide.jsp#ConfiguringWebXml'>User Guide</tt> 
027     for more information.</span>
028     
029     <P>Most formats are localized using the {@link java.util.Locale} passed to this object. 
030     See {@link LocaleSource} for more information.  
031    
032     <P>These formats are intended for implementing standard formats for display of 
033     data both in forms ({@link hirondelle.web4j.ui.tag.Populate}) and in 
034     listings ({@link hirondelle.web4j.database.Report}).
035     
036     <P>See also {@link DateConverter}, which is also used by this class.
037    */
038    public final class Formats {
039      
040      /**
041       Construct with a {@link Locale} and {@link TimeZone} to be applied to non-localized patterns. 
042       
043       @param aLocale almost always comes from {@link LocaleSource}.
044       @param aTimeZone almost always comes from {@link TimeZoneSource}. A defensive copy is made of 
045       this mutable object.
046      */
047      public Formats(Locale aLocale, TimeZone aTimeZone){
048        fLocale = aLocale;
049        fTimeZone = TimeZone.getTimeZone(aTimeZone.getID()); //defensive copy
050        fDateConverter = BuildImpl.forDateConverter();
051      }
052      
053      /** Return the {@link Locale} passed to the constructor.  */
054      public Locale getLocale(){
055        return fLocale;
056      }
057      
058      /** Return a TimeZone of the same id as the one passed to the constructor.  */
059      public TimeZone getTimeZone(){
060        return TimeZone.getTimeZone(fTimeZone.getID());
061      }
062      
063      /** Return the format in which {@link BigDecimal}s and {@link Decimal}s are displayed in a form.  */
064      public DecimalFormat getBigDecimalDisplayFormat(){
065        return getDecimalFormat(fConfig.getBigDecimalDisplayFormat());
066      }
067      
068      /**
069       Return the regular expression for validating the format of numeric amounts input by the user, having a 
070       possible decimal portion, with any number of decimals.
071        
072       <P>The returned {@link Pattern} is controlled by a setting in <tt>web.xml</tt>, 
073       for decimal separator(s). It is suitable for both {@link Decimal} and {@link BigDecimal} values.
074       This item is not affected by a {@link Locale}. 
075       
076       <P>See <tt>web.xml</tt> for more information. 
077      */
078      public Pattern getDecimalInputFormat(){
079        return fConfig.getDecimalInputPattern();
080      }
081      
082      /** Return the format in which integer amounts are displayed in a report.  */
083      public DecimalFormat getIntegerReportDisplayFormat(){
084        return getDecimalFormat(fConfig.getIntegerDisplayFormat());
085      }
086      
087      /**
088       Return the text used to render boolean values in a report.
089       
090       <P>The return value does not depend on {@link Locale}.
091      */
092      public static String getBooleanDisplayText(Boolean aBoolean){
093        Config config = new Config();
094        return aBoolean ? config.getBooleanTrueDisplayFormat() : config.getBooleanFalseDisplayFormat();
095      }
096    
097      /**
098       Return the text used to render empty or <tt>null</tt> values in a report.
099       
100       <P>The return value does not depend on {@link Locale}. See <tt>web.xml</tt> for more information.
101      */
102      public static String getEmptyOrNullText() {
103        return new Config().getEmptyOrNullDisplayFormat();
104      }
105      
106      /**
107       Translate an object into text, suitable for presentation <em>in an HTML form</em>.
108       
109       <P>The intent of this method is to return values matching those POSTed during form submission, 
110       not the visible text presented to the user. 
111       
112       <P>The returned text is not escaped in any way.
113       That is, <em>if special characters need to be escaped, the caller must perform the escaping</em>.
114       
115       <P>Apply these policies in the following order :
116      <ul>
117       <li>if <tt>null</tt>, return an empty <tt>String</tt>
118       <li>if a {@link DateTime}, apply {@link DateConverter#formatEyeFriendlyDateTime(DateTime, Locale)}
119       <li>if a {@link Date}, apply {@link DateConverter#formatEyeFriendly(Date, Locale, TimeZone)}
120       <li>if a {@link BigDecimal}, display in the form of {@link BigDecimal#toString}, with 
121       one exception : the decimal separator will be as configured in <tt>web.xml</tt>. 
122       (If the setting for the decimal separator allows for <em>both</em> a period and a comma, 
123       then a period is used.)
124       <li>if a {@link Decimal}, display the amount only, using the same rendering as for <tt>BigDecimal</tt> 
125       <li>if a {@link TimeZone}, return {@link TimeZone#getID()}
126       <li>if a {@link Code}, return {@link Code#getId()}.toString()
127       <li>if a {@link Id}, return {@link Id#getRawString()}
128       <li>if a {@link SafeText}, return {@link SafeText#getRawString()}
129       <li>otherwise, return <tt>aObject.toString()</tt>
130      </ul>
131       
132       <P>If <tt>aObject</tt> is a <tt>Collection</tt>, then the caller must call 
133       this method for every element in the <tt>Collection</tt>.
134      
135       @param aObject must not be a <tt>Collection</tt>.
136      */
137      public String objectToText(Object aObject) {
138        String result = null;
139        if ( aObject == null ){
140          result = Consts.EMPTY_STRING;
141        }
142        else if ( aObject instanceof DateTime ){
143          DateTime dateTime = (DateTime)aObject;
144          result = fDateConverter.formatEyeFriendlyDateTime(dateTime, fLocale);
145        }
146        else if ( aObject instanceof Date ){
147          Date date = (Date)aObject;
148          result = fDateConverter.formatEyeFriendly(date, fLocale, fTimeZone);
149        }
150        else if ( aObject instanceof BigDecimal ){
151          BigDecimal amount = (BigDecimal)aObject;
152          result = renderBigDecimal(amount);
153        }
154        else if ( aObject instanceof Decimal ){
155          Decimal money = (Decimal)aObject;
156          result = renderBigDecimal(money.getAmount());
157        }
158        else if ( aObject instanceof TimeZone ) {
159          TimeZone timeZone = (TimeZone)aObject;
160          result = timeZone.getID();
161        }
162        else if ( aObject instanceof Code ) {
163          Code code = (Code)aObject;
164          result = code.getId().getRawString();
165        }
166        else if ( aObject instanceof Id ) {
167          Id id = (Id)aObject;
168          result = id.getRawString();
169        }
170        else if ( aObject instanceof SafeText ) {
171          //The Populate tag will safely escape all such text data.
172          //To avoid double escaping, the raw form is returned.
173          SafeText safeText = (SafeText)aObject;
174          result = safeText.getRawString();
175        }
176        else {
177          result = aObject.toString();
178        }
179        return result;
180      }
181      
182      /**
183       Translate an object into text suitable for direct presentation in a JSP.
184       
185       <P>In general, a report can be rendered in various ways: HTML, XML, plain text. 
186       Each of these styles has different needs for escaping special characters. 
187       This method returns a {@link SafeText}, which can escape characters in 
188       various ways. 
189       
190       <P>This method applies the following policies to get the <em>unescaped</em> text :
191       <P>
192       <table border=1 cellpadding=3 cellspacing=0>
193        <tr><th>Type</th> <th>Action</th></tr>
194        <tr>   
195         <td><tt>SafeText</tt></td> 
196         <td>use {@link SafeText#getRawString()}</td>
197        </tr>
198        <tr>   
199         <td><tt>Id</tt></td> 
200         <td>use {@link Id#getRawString()}</td>
201        </tr>
202        <tr>   
203         <td><tt>Code</tt></td> 
204         <td>use {@link Code#getText()}.getRawString()</td>
205        </tr>
206        <tr>
207         <td><tt>hirondelle.web4.model.DateTime</tt></td> 
208         <td>apply {@link DateConverter#formatEyeFriendlyDateTime(DateTime, Locale)} </td>
209        </tr>
210        <tr>
211         <td><tt>java.util.Date</tt></td> 
212         <td>apply {@link DateConverter#formatEyeFriendly(Date, Locale, TimeZone)} </td>
213        </tr>
214        <tr>
215         <td><tt>BigDecimal</tt></td> 
216         <td>use {@link #getBigDecimalDisplayFormat} </td>
217        </tr>
218        <tr>
219         <td><tt>Decimal</tt></td> 
220         <td>use {@link #getBigDecimalDisplayFormat} on <tt>decimal.getAmount()</tt></td>
221        </tr>
222        <tr>
223         <td><tt>Boolean</tt></td> 
224         <td>use {@link #getBooleanDisplayText} </td>
225        </tr>
226        <tr>
227         <td><tt>Integer</tt></td> 
228         <td>use {@link #getIntegerReportDisplayFormat} </td>
229        </tr>
230        <tr>
231         <td><tt>Long</tt></td> 
232         <td>use {@link #getIntegerReportDisplayFormat} </td>
233        </tr>
234        <tr>   
235         <td><tt>Locale</tt></td> 
236         <td>use {@link Locale#getDisplayName(java.util.Locale)} </td>
237        </tr>
238        <tr>   
239         <td><tt>TimeZone</tt></td> 
240         <td>use {@link TimeZone#getDisplayName(boolean, int, java.util.Locale)} (with no daylight savings hour, and in the <tt>SHORT</tt> style </td>
241        </tr>
242        <tr>
243         <td>..other...</td> 
244         <td>
245           use <tt>toString</tt>, and pass result to constructor of {@link SafeText}. 
246         </td>
247        </tr>
248       </table>
249      
250       <P>In addition, the value returned by {@link #getEmptyOrNullText} is used if :
251       <ul>
252       <li><tt>aObject</tt> is itself <tt>null</tt>
253       <li>the result of the above policies returns text which has no content
254      </ul>
255      */
256      public SafeText objectToTextForReport(Object aObject) {
257        String result = null;
258        if ( aObject == null ){
259          result = null;
260        }
261        else if (aObject instanceof SafeText){
262          //it is odd to extract an identical object like this, 
263          //but it safely avoids double escaping at the end of this method
264          SafeText text = (SafeText) aObject;
265          result = text.getRawString();
266        }
267        else if (aObject instanceof Id){
268          Id id = (Id) aObject;
269          result = id.getRawString();
270        }
271        else if (aObject instanceof Code){
272          Code code = (Code) aObject;
273          result = code.getText().getRawString();
274        }
275        else if (aObject instanceof String) {
276          result = aObject.toString();
277        }
278        else if ( aObject instanceof DateTime ){
279          DateTime dateTime = (DateTime)aObject;
280          result = fDateConverter.formatEyeFriendlyDateTime(dateTime, fLocale);
281        }
282        else if ( aObject instanceof Date ){
283          Date date = (Date)aObject;
284          result = fDateConverter.formatEyeFriendly(date, fLocale, fTimeZone);
285        }
286        else if ( aObject instanceof BigDecimal ){
287          BigDecimal amount = (BigDecimal)aObject;
288          result = getBigDecimalDisplayFormat().format(amount.doubleValue());
289        }
290        else if ( aObject instanceof Decimal ){
291          Decimal money = (Decimal)aObject;
292          result = getBigDecimalDisplayFormat().format(money.getAmount().doubleValue());
293        }
294        else if ( aObject instanceof Boolean ){
295          Boolean value = (Boolean)aObject;
296          result = getBooleanDisplayText(value);
297        }
298        else if ( aObject instanceof Integer ) {
299          Integer value = (Integer)aObject;
300          result = getIntegerReportDisplayFormat().format(value);
301        }
302        else if ( aObject instanceof Long ) {
303          Long value = (Long)aObject;
304          result = getIntegerReportDisplayFormat().format(value.longValue());
305        }
306        else if ( aObject instanceof Locale ) {
307          Locale locale = (Locale)aObject;
308          result = locale.getDisplayName(fLocale);
309        }
310        else if ( aObject instanceof TimeZone ) {
311          TimeZone timeZone = (TimeZone)aObject;
312          result = timeZone.getDisplayName(false, TimeZone.SHORT, fLocale);
313        }
314        else {
315          result = aObject.toString();
316        }
317        //ensure that all empty results have configured content
318        if ( ! Util.textHasContent(result) ) {
319          result = fConfig.getEmptyOrNullDisplayFormat();
320        }
321        return new SafeText(result);
322      }
323      
324      // PRIVATE
325      private final Locale fLocale;
326      private final TimeZone fTimeZone;
327      private final DateConverter fDateConverter;
328      private Config fConfig = new Config();
329     
330      private DecimalFormat getDecimalFormat(String aFormat){
331        DecimalFormat result = null;
332        NumberFormat format = NumberFormat.getNumberInstance(fLocale);
333        if (format instanceof DecimalFormat){
334          result = (DecimalFormat)format;
335        }
336        else {
337          throw new AssertionError();
338        }
339        result.applyPattern(aFormat);
340        return result;
341      }
342      
343      /**
344       Return the pattern applicable to numeric input of a number with a possible decimal portion. 
345      */
346      private String replacePeriodWithComma(String aValue){
347        return aValue.replace(".", ",");
348      }
349      
350      private String renderBigDecimal(BigDecimal aBigDecimal){
351        String result = aBigDecimal.toPlainString();
352        if( "COMMA".equalsIgnoreCase(fConfig.getDecimalSeparator()) ){
353          result = replacePeriodWithComma(result);
354        }
355        return result;
356      }
357      
358      /** Informal test harness.   */
359      private static void main(String... args){
360        Formats formats = new Formats(Locale.CANADA, TimeZone.getTimeZone("Canada/Atlantic"));
361        System.out.println("en_fr: " + formats.objectToTextForReport(new Locale("en_fr")));
362        System.out.println("Canada/Pacific: " + formats.objectToTextForReport(TimeZone.getTimeZone("Canada/Pacific")));
363      }
364    }