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 }