Package hirondelle.web4j.request

Source Code of hirondelle.web4j.request.Formats

package hirondelle.web4j.request;

import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.ServletConfig;

import hirondelle.web4j.readconfig.InitParam;
import hirondelle.web4j.request.DateConverter;
import hirondelle.web4j.security.SafeText;
import hirondelle.web4j.util.Consts;
import hirondelle.web4j.util.Regex;
import hirondelle.web4j.util.Util;
import hirondelle.web4j.model.Code;
import hirondelle.web4j.model.DateTime;
import hirondelle.web4j.model.Id;
import hirondelle.web4j.model.Decimal;
import hirondelle.web4j.BuildImpl;
import hirondelle.web4j.TESTAll;

/**
Standard display formats for the application.
<P>The formats used by this class are <em>mostly</em> configured in
<tt>web.xml</tt>, and are read by this class upon startup.
<span class="highlight">See the <a href='http://www.web4j.com/UserGuide.jsp#ConfiguringWebXml'>User Guide</tt>
for more information.</span>
<P>Most formats are localized using the {@link java.util.Locale} passed to this object.
See {@link LocaleSource} for more information. 

<P>These formats are intended for implementing standard formats for display of
data both in forms ({@link hirondelle.web4j.ui.tag.Populate}) and in
listings ({@link hirondelle.web4j.database.Report}).
<P>See also {@link DateConverter}, which is also used by this class.
*/
public final class Formats {
 
  /** Called only from {@link RequestParser}, upon startup. */
  /*(TESTING from FormPopulator) public */ static void init(ServletConfig aConfig){
    fBigDecimalDisplayFormat = fBIG_DECIMAL_DISPLAY_FORMAT.fetch(aConfig).getValue();
    fDecimalSeparator = fDECIMAL_SEPARATOR.fetch(aConfig).getValue();
    fIntegerFormat = fINTEGER_FORMAT.fetch(aConfig).getValue();
    fBooleanTrueText = fBOOLEAN_TRUE_TEXT.fetch(aConfig).getValue();
    fBooleanFalseText = fBOOLEAN_FALSE_TEXT.fetch(aConfig).getValue();
    fEmptyOrNullText = fEMPTY_OR_NULL_TEXT.fetch(aConfig).getValue();
    fDecimalRegex = buildDecimalFormat();
  }

  /**
   Construct with a {@link Locale} and {@link TimeZone} to be applied to non-localized patterns.
  
   @param aLocale almost always comes from {@link LocaleSource}.
   @param aTimeZone almost always comes from {@link TimeZoneSource}. A defensive copy is made of
   this mutable object.
  */
  public Formats(Locale aLocale, TimeZone aTimeZone){
    fLocale = aLocale;
    fTimeZone = TimeZone.getTimeZone(aTimeZone.getID()); //defensive copy
    if(TESTAll.IS_TESTING) {
      fDateConverter = new TestingImpl();
    }
    else {
      fDateConverter = BuildImpl.forDateConverter();
    }
  }
 
  /** Return the {@link Locale} passed to the constructor.  */
  public Locale getLocale(){
    return fLocale;
  }
 
  /** Return a TimeZone of the same id as the one passed to the constructor.  */
  public TimeZone getTimeZone(){
    return TimeZone.getTimeZone(fTimeZone.getID());
  }
 
  /** Return the format in which {@link BigDecimal}s and {@link Decimal}s are displayed in a form.  */
  public DecimalFormat getBigDecimalDisplayFormat(){
    return getDecimalFormat(fBigDecimalDisplayFormat);
  }
 
  /**
   Return the regular expression for validating the format of numeric amounts input by the user, having a
   possible decimal portion, with any number of decimals.
   
   <P>The returned {@link Pattern} is controlled by a setting in <tt>web.xml</tt>,
   for decimal separator(s). It is suitable for both {@link Decimal} and {@link BigDecimal} values.
   This item is not affected by a {@link Locale}.
  
   <P>See <tt>web.xml</tt> for more information.
  */
  public Pattern getDecimalInputFormat(){
    return fDecimalRegex;
  }
 
  /** Return the format in which integer amounts are displayed in a report.  */
  public DecimalFormat getIntegerReportDisplayFormat(){
    return getDecimalFormat(fIntegerFormat);
  }
 
  /**
   Return the text used to render boolean values in a report.
  
   <P>The return value does not depend on {@link Locale}.
  */
  public static String getBooleanDisplayText(Boolean aBoolean){
    return aBoolean ? fBooleanTrueText : fBooleanFalseText;
  }

  /**
   Return the text used to render empty or <tt>null</tt> values in a report.
  
   <P>The return value does not depend on {@link Locale}. See <tt>web.xml</tt> for more information.
  */
  public static String getEmptyOrNullText() {
    return fEmptyOrNullText;
  }
 
  /**
   Translate an object into text, suitable for presentation <em>in an HTML form</em>.
  
   <P>The intent of this method is to return values matching those POSTed during form submission,
   not the visible text presented to the user.
  
   <P>The returned text is not escaped in any way.
   That is, <em>if special characters need to be escaped, the caller must perform the escaping</em>.
  
   <P>Apply these policies in the following order :
  <ul>
   <li>if <tt>null</tt>, return an empty <tt>String</tt>
   <li>if a {@link DateTime}, apply {@link DateConverter#formatEyeFriendlyDateTime(DateTime, Locale)}
   <li>if a {@link Date}, apply {@link DateConverter#formatEyeFriendly(Date, Locale, TimeZone)}
   <li>if a {@link BigDecimal}, display in the form of {@link BigDecimal#toString}, with
   one exception : the decimal separator will be as configured in <tt>web.xml</tt>.
   (If the setting for the decimal separator allows for <em>both</em> a period and a comma,
   then a period is used.)
   <li>if a {@link Decimal}, display the amount only, using the same rendering as for <tt>BigDecimal</tt>
   <li>if a {@link TimeZone}, return {@link TimeZone#getID()}
   <li>if a {@link Code}, return {@link Code#getId()}.toString()
   <li>if a {@link Id}, return {@link Id#getRawString()}
   <li>if a {@link SafeText}, return {@link SafeText#getRawString()}
   <li>otherwise, return <tt>aObject.toString()</tt>
  </ul>
  
   <P>If <tt>aObject</tt> is a <tt>Collection</tt>, then the caller must call
   this method for every element in the <tt>Collection</tt>.
 
   @param aObject must not be a <tt>Collection</tt>.
  */
  public String objectToText(Object aObject) {
    String result = null;
    if ( aObject == null ){
      result = Consts.EMPTY_STRING;
    }
    else if ( aObject instanceof DateTime ){
      DateTime dateTime = (DateTime)aObject;
      result = fDateConverter.formatEyeFriendlyDateTime(dateTime, fLocale);
    }
    else if ( aObject instanceof Date ){
      Date date = (Date)aObject;
      result = fDateConverter.formatEyeFriendly(date, fLocale, fTimeZone);
    }
    else if ( aObject instanceof BigDecimal ){
      BigDecimal amount = (BigDecimal)aObject;
      result = renderBigDecimal(amount);
    }
    else if ( aObject instanceof Decimal ){
      Decimal money = (Decimal)aObject;
      result = renderBigDecimal(money.getAmount());
    }
    else if ( aObject instanceof TimeZone ) {
      TimeZone timeZone = (TimeZone)aObject;
      result = timeZone.getID();
    }
    else if ( aObject instanceof Code ) {
      Code code = (Code)aObject;
      result = code.getId().getRawString();
    }
    else if ( aObject instanceof Id ) {
      Id id = (Id)aObject;
      result = id.getRawString();
    }
    else if ( aObject instanceof SafeText ) {
      //The Populate tag will safely escape all such text data.
      //To avoid double escaping, the raw form is returned.
      SafeText safeText = (SafeText)aObject;
      result = safeText.getRawString();
    }
    else {
      result = aObject.toString();
    }
    return result;
  }
 
  /**
   Translate an object into text suitable for direct presentation in a JSP.
  
   <P>In general, a report can be rendered in various ways: HTML, XML, plain text.
   Each of these styles has different needs for escaping special characters.
   This method returns a {@link SafeText}, which can escape characters in
   various ways.
  
   <P>This method applies the following policies to get the <em>unescaped</em> text :
   <P>
   <table border=1 cellpadding=3 cellspacing=0>
    <tr><th>Type</th> <th>Action</th></tr>
    <tr>  
     <td><tt>SafeText</tt></td>
     <td>use {@link SafeText#getRawString()}</td>
    </tr>
    <tr>  
     <td><tt>Id</tt></td>
     <td>use {@link Id#getRawString()}</td>
    </tr>
    <tr>  
     <td><tt>Code</tt></td>
     <td>use {@link Code#getText()}.getRawString()</td>
    </tr>
    <tr>
     <td><tt>hirondelle.web4.model.DateTime</tt></td>
     <td>apply {@link DateConverter#formatEyeFriendlyDateTime(DateTime, Locale)} </td>
    </tr>
    <tr>
     <td><tt>java.util.Date</tt></td>
     <td>apply {@link DateConverter#formatEyeFriendly(Date, Locale, TimeZone)} </td>
    </tr>
    <tr>
     <td><tt>BigDecimal</tt></td>
     <td>use {@link #getBigDecimalDisplayFormat} </td>
    </tr>
    <tr>
     <td><tt>Decimal</tt></td>
     <td>use {@link #getBigDecimalDisplayFormat} on <tt>decimal.getAmount()</tt></td>
    </tr>
    <tr>
     <td><tt>Boolean</tt></td>
     <td>use {@link #getBooleanDisplayText} </td>
    </tr>
    <tr>
     <td><tt>Integer</tt></td>
     <td>use {@link #getIntegerReportDisplayFormat} </td>
    </tr>
    <tr>
     <td><tt>Long</tt></td>
     <td>use {@link #getIntegerReportDisplayFormat} </td>
    </tr>
    <tr>  
     <td><tt>Locale</tt></td>
     <td>use {@link Locale#getDisplayName(java.util.Locale)} </td>
    </tr>
    <tr>  
     <td><tt>TimeZone</tt></td>
     <td>use {@link TimeZone#getDisplayName(boolean, int, java.util.Locale)} (with no daylight savings hour, and in the <tt>SHORT</tt> style </td>
    </tr>
    <tr>
     <td>..other...</td>
     <td>
       use <tt>toString</tt>, and pass result to constructor of {@link SafeText}.
     </td>
    </tr>
   </table>
 
   <P>In addition, the value returned by {@link #getEmptyOrNullText} is used if :
   <ul>
   <li><tt>aObject</tt> is itself <tt>null</tt>
   <li>the result of the above policies returns text which has no content
  </ul>
  */
  public SafeText objectToTextForReport(Object aObject) {
    String result = null;
    if ( aObject == null ){
      result = null;
    }
    else if (aObject instanceof SafeText){
      //it is odd to extract an identical object like this,
      //but it safely avoids double escaping at the end of this method
      SafeText text = (SafeText) aObject;
      result = text.getRawString();
    }
    else if (aObject instanceof Id){
      Id id = (Id) aObject;
      result = id.getRawString();
    }
    else if (aObject instanceof Code){
      Code code = (Code) aObject;
      result = code.getText().getRawString();
    }
    else if (aObject instanceof String) {
      result = aObject.toString();
    }
    else if ( aObject instanceof DateTime ){
      DateTime dateTime = (DateTime)aObject;
      result = fDateConverter.formatEyeFriendlyDateTime(dateTime, fLocale);
    }
    else if ( aObject instanceof Date ){
      Date date = (Date)aObject;
      result = fDateConverter.formatEyeFriendly(date, fLocale, fTimeZone);
    }
    else if ( aObject instanceof BigDecimal ){
      BigDecimal amount = (BigDecimal)aObject;
      result = getBigDecimalDisplayFormat().format(amount.doubleValue());
    }
    else if ( aObject instanceof Decimal ){
      Decimal money = (Decimal)aObject;
      result = getBigDecimalDisplayFormat().format(money.getAmount().doubleValue());
    }
    else if ( aObject instanceof Boolean ){
      Boolean value = (Boolean)aObject;
      result = getBooleanDisplayText(value);
    }
    else if ( aObject instanceof Integer ) {
      Integer value = (Integer)aObject;
      result = getIntegerReportDisplayFormat().format(value);
    }
    else if ( aObject instanceof Long ) {
      Long value = (Long)aObject;
      result = getIntegerReportDisplayFormat().format(value.longValue());
    }
    else if ( aObject instanceof Locale ) {
      Locale locale = (Locale)aObject;
      result = locale.getDisplayName(fLocale);
    }
    else if ( aObject instanceof TimeZone ) {
      TimeZone timeZone = (TimeZone)aObject;
      result = timeZone.getDisplayName(false, TimeZone.SHORT, fLocale);
    }
    else {
      result = aObject.toString();
    }
    //ensure that all empty results have configured content
    if ( ! Util.textHasContent(result) ) {
      result = fEmptyOrNullText;
    }
    return new SafeText(result);
  }
 
  // PRIVATE //
  private final Locale fLocale;
  private final TimeZone fTimeZone;
  private final DateConverter fDateConverter;
  private static Pattern fDecimalRegex;
  private static final InitParam fBIG_DECIMAL_DISPLAY_FORMAT = new InitParam("BigDecimalDisplayFormat", "#,##0.00");
  private static String fBigDecimalDisplayFormat;

  private static final InitParam fDECIMAL_SEPARATOR = new InitParam("DecimalSeparator", "PERIOD");
  private static String fDecimalSeparator;
 
  private static final InitParam fBOOLEAN_TRUE_TEXT = new InitParam("BooleanTrueDisplayFormat", "<input type='checkbox' name='true' value='true' checked readonly notab>");
  private static String fBooleanTrueText;
 
  private static final InitParam fBOOLEAN_FALSE_TEXT = new InitParam("BooleanFalseDisplayFormat", "<input type='checkbox' name='false' value='false' readonly notab>");
  private static String fBooleanFalseText;
 
  private static final InitParam fEMPTY_OR_NULL_TEXT = new InitParam("EmptyOrNullDisplayFormat", "-");
  private static String fEmptyOrNullText;

  private static final InitParam fINTEGER_FORMAT = new InitParam("IntegerDisplayFormat", "#,###");
  private static String fIntegerFormat;
 
  private static final String COMMA = "COMMA";
  private static final String PERIOD = "PERIOD";
  private static final String PERIOD_OR_COMMA = "PERIOD,COMMA";
 
  private static final Logger fLogger = Util.getLogger(Formats.class);
 
  private DecimalFormat getDecimalFormat(String aFormat){
    DecimalFormat result = null;
    NumberFormat format = NumberFormat.getNumberInstance(fLocale);
    if (format instanceof DecimalFormat){
      result = (DecimalFormat)format;
    }
    else {
      throw new AssertionError();
    }
    result.applyPattern(aFormat);
    return result;
  }
 
  private static void vomit(String aMessage){
    fLogger.severe(aMessage);
    throw new IllegalArgumentException(aMessage);
  }
 
  /**
   Return the pattern applicable to numeric input of a number with a possible decimal portion.
  */
  private static Pattern buildDecimalFormat(){
    String pattern = "";
    String sign = "(?:-|\\+)?";
    String digits = "[0-9]+";
    String decimalSign = getDecimalSignPattern();
    String places  = "[0-9]+";
   
    // pattern = sign?(digits|digits.places|.places)
    pattern = sign + "(" + digits + Regex.OR + digits + decimalSign + places + Regex.OR + decimalSign + places + ")";
    return Pattern.compile(pattern);
  }
 
  private static String getDecimalSignPattern(){
    String result = null;
    if( PERIOD.equalsIgnoreCase(fDecimalSeparator) ) {
      result = "(?:\\.)";
    }
    else if ( COMMA.equalsIgnoreCase(fDecimalSeparator) ){
      result = "(?:,)";
    }
    else if ( PERIOD_OR_COMMA.equalsIgnoreCase(fDecimalSeparator)){
      result = "(?:\\.|,)";
    }
    else {
      vomit(
        "In web.xml, the setting for DecimalSeparator is not in the expected format. " +
        "See web.xml for more information."
      );
    }
    return result;
  }
 
  private String replacePeriodWithComma(String aValue){
    return aValue.replace(".", ",");
  }
 
  /** For TESTING only.  */ 
  private static final class TestingImpl implements DateConverter {
    //Dates:
    public String formatEyeFriendly(Date aDate, Locale aLocale, TimeZone aTimeZone) {
      SimpleDateFormat format = new SimpleDateFormat("MM/dd/yyyy");
      //format.setTimeZone(aTimeZone);
      return format.format(aDate);
    }
    public Date parseHandFriendly(String aInputValue, Locale aLocale, TimeZone aTimeZone) {
      return parseDate(aInputValue, HAND_FRIENDLY_REGEX);
    }
    public Date parseEyeFriendly(String aInputValue, Locale aLocale, TimeZone aTimeZone) {
      return parseDate(aInputValue, EYE_FRIENDLY_REGEX);
    }
    //DateTimes:
    public String formatEyeFriendlyDateTime(DateTime aDateTime, Locale aLocale) {
      return aDateTime.format("MM/DD/YYYY", aLocale);
    }
    public DateTime parseEyeFriendlyDateTime(String aInputValue, Locale aLocale) {
      return parseDateTime(aInputValue, EYE_FRIENDLY_REGEX);
    }
    public DateTime parseHandFriendlyDateTime(String aInputValue, Locale aLocale) {
      return parseDateTime(aInputValue, HAND_FRIENDLY_REGEX);
    }
    private static final Pattern HAND_FRIENDLY_REGEX =
     Pattern.compile(Regex.MONTH + Regex.DAY_OF_MONTH + "(\\d\\d\\d\\d)");
    ;
    private static final Pattern EYE_FRIENDLY_REGEX =
      Pattern.compile(Regex.MONTH + "/" + Regex.DAY_OF_MONTH + "/" + "(\\d\\d\\d\\d)")
    ;
    private Date parseDate(String aInputValue, Pattern aRegex){
      Date result = null;
      Matcher matcher = aRegex.matcher(aInputValue);
      if( matcher.matches() ) {
        Integer month = new Integer(matcher.group(Regex.FIRST_GROUP));
        Integer day = new Integer(matcher.group(Regex.SECOND_GROUP));
        Integer year = new Integer( matcher.group(Regex.THIRD_GROUP) );
        Calendar cal = new GregorianCalendar(year.intValue(), month.intValue() - 1, day.intValue(), 0,0,0);
        result = cal.getTime();
      }
      return result;
    }
    private DateTime parseDateTime(String aInputValue, Pattern aRegex){
      DateTime result = null;
      Matcher matcher = aRegex.matcher(aInputValue);
      if( matcher.matches() ) {
        Integer month = new Integer(matcher.group(Regex.FIRST_GROUP));
        Integer day = new Integer(matcher.group(Regex.SECOND_GROUP));
        Integer year = new Integer( matcher.group(Regex.THIRD_GROUP) );
        result = DateTime.forDateOnly(year, month, day);
      }
      return result;
    }
  }
 
  private String renderBigDecimal(BigDecimal aBigDecimal){
    String result = aBigDecimal.toPlainString();
    if( COMMA.equalsIgnoreCase(fDecimalSeparator) ){
      result = replacePeriodWithComma(result);
    }
    return result;
  }
 
  /** Informal test harness.   */
  private static void main(String... args){
    init(null);
    Formats formats = new Formats(Locale.CANADA, TimeZone.getTimeZone("Canada/Atlantic"));
    System.out.println("en_fr: " + formats.objectToTextForReport(new Locale("en_fr")));
    System.out.println("Canada/Pacific: " + formats.objectToTextForReport(TimeZone.getTimeZone("Canada/Pacific")));
  }
}
TOP

Related Classes of hirondelle.web4j.request.Formats

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.