6 Replies Latest reply on Dec 7, 2010 7:03 PM by gebuh

    rich:calendar dateConverter and list sorting

    gebuh Newbie

      This calendar stuff is killing me, appreciate any advice.
      System: Seam 2.2.0 richfaces 3.3.1 running on jboss 5.1.0
      I have a search page that allows a user to search between dates, when I used datePattern="MM/dd/yyyy" and the user manually entered a date like 12/01/09 the date was changed on loss of focus to 12/01/0009.  Ok, so I tried using a dateConverter and registered it in page.xml:



      <param name="endHireDate" value="#{employeeVList.employeeV.endHireDate}"
       converterId="pathToMyConverter.action.DateConverter"/>





      This is the same converter as org.jboss.seam.faces.dateConverter I just copied it into my application for some troubleshooting.
      But unless I also changed the datePattern in the component to MM/dd/yy(using a 2 digit format), search still didn't work - I just got back all results.
      No problem, I'll just use a 2 digit year.  However, when I do this I get back results whether they are 2009 or 0009(some bad data in the test db as a result of not having found this problem til recently).
      So what am I doing wrong?
      Here's one of the calendar components:



      <s:decorate id="afterDateHire" template="../layout/display.xhtml">
      <ui:define name="label"> Hire Date From:</ui:define>
       <rich:calendar id="afterDateHire" datePattern="MM/dd/yy"
                     value="#{employeeVList.employeeV.startHireDate}" enableManualInput = "true">
         <a:support event="onchange" ajaxSingle = "true" bypassUpdates="false">
           <f:param name="startHireDate" value="#{employeeVList.employeeV.startHireDate}"/>
         </a:support>
       </rich:calendar>
      </s:decorate>




        • 1. Re: rich:calendar dateConverter and list sorting
          Tim Evers Master

          Honestly when it comes to dates and supporting your own formats it is just easier to write your own converter.


          Because I'm feeling nice I'll give you mine. :P All you need to do is add the formats you want to use and fix up the messages etc.


          @Name(MyDateConverter.SEAM_ID)
          @Scope(ScopeType.EVENT)
          @Install(precedence = Install.APPLICATION)
          @Converter(id = "GekkoDateConverter", forClass = Date.class)
          @BypassInterceptors
          public class MyDateConverter
              implements javax.faces.convert.Converter, Serializable
          {
              public static final String SEAM_ID = "DateConverter";
              public static final String CONVERTER_ID = "gekko.web.converter.DateConverter";
          
              public static final String DATE_ID = MyDateConverter.class.getName() + ".DATE";
              public static final String STRING_ID = MyDateConverter.class.getName() + ".STRING";
          
              @Override
              public Object getAsObject(FacesContext context,
                  UIComponent component,
                  String value)
              {
                  if (StringUtils.isBlank(value)) { //Just a util method that checks if the string is null or blank
                      return null;
                  }
                  
                  List<String> acceptedFormats = new ArrayList<String>();
                  acceptedFormats.add("dd/MM/yyyy");
                  acceptedFormats.add("ddMMyyyy");
                  //Always check these 2 formats FIRST
                  acceptedFormats.add(0,"dd/MM/yy");
                  acceptedFormats.add(0,"ddMMyy");
                  
                  for (String format : acceptedFormats) {
                      try {
                          return MyDateConverter.parseDate(value, format, "");
                      } catch (IllegalArgumentException e) {
                          // parse failed but we need to check all accepted formats first
                      }
                  }
                  String accptedFormatsParam = "dd/MM/yyyy, ddMMyyyy, dd/MM/yy, ddMMyy"; 
          
                  //Obviously you need to create your own message here.
                  FacesMessage message = MessageFactory.getMessage(
                      context,
                      DATE_ID, 
                      value, 
                      accptedFormatsParam, 
                      new Date(), 
                      MessageFactory.getLabel(context, component));
                      
                  throw new ConverterException(message);
              }
          
              @Override
              public String getAsString(FacesContext context,
                  UIComponent component,
                  Object value)
              {
                  if (value == null) {
                      return "";
                  }
                  try {
                      return 
                          MyDateConverter.formatDate(
                              ((Date) value).getTime(),
                              "dd/MM/yyyy");
                  } catch (ConverterException e) {
                      throw new ConverterException(
                          MessageFactory.getMessage(
                              context,
                              STRING_ID, 
                              value, 
                              MessageFactory.getLabel(context, component)), e);
                  }
              }
          
              private static String formatDate(long millis, String format) {
                  SimpleDateFormat df = new SimpleDateFormat(format);
                  df.setLenient(false);
                  return df.format(new Date(millis));
              }
              
              public static Date parseDate(
                  String dateString,
                  String dateFormat,
                  String nullDateFlag)
              {
                  //These methods just throw an exception if the values are null
                  Validate.notNull(nullDateFlag, "param nullDateFlag may not be null");
                  Validate.notNull(dateFormat, "param dateFormat may not be null");
              
                  if( dateString == null ){
                      return null;
                  }
              
                  if (nullDateFlag.equalsIgnoreCase(dateString)) {
                      return null;
                  }
              
                  SimpleDateFormat df = new SimpleDateFormat(dateFormat);
                  df.setLenient(false);
                  
                  //Any dates with 2 digit year > 50 will be considered in the 1900s
                  df.set2DigitYearStart(new GregorianCalendar(1950, 0, 1).getTime()); 
                  
                  try {
                      Date parsedDate = df.parse(dateString);
                      Calendar cal = Calendar.getInstance();
                      cal.setTime(parsedDate);
              
                      if (cal.get(Calendar.YEAR) > 9999) {
                          throw new IllegalArgumentException("date '" + dateString
                              + "' cannot have year greater then 9999 '");
                      }
              
                      return parsedDate;
                  } catch (ParseException e) {
                      throw new IllegalArgumentException("date '" + dateString
                          + "' was not in format '" + dateFormat + "'", e);
                  }
              }
          }
          

          • 2. Re: rich:calendar dateConverter and list sorting
            gebuh Newbie

            thank you kindly Tim, but I still have the same problem. 
            converter added to faces-config.xml:



            <converter>
            <converter-id>pathTo.action.MyDateConverter</converter-id>
            <converter-for-class>java.util.Date</converter-for-class>
            <converter-class>pathTo.action.MyDateConverter</converter-class>
            </converter>



            I thought that using the converter-for-class tag would call the converter for all objects of type date, but that does not appear to be happening


            if I use the converter in the component:



            <s:decorate id="afterDateHire" template="../layout/display.xhtml">
            <ui:define name="label"> Hire Date From:</ui:define>
            <rich:calendar id="afterDateHire" datePattern="MM/dd/yy"
            value="#{employeeVList.employeeV.startHireDate}" enableManualInput = "true"
            converter = "#{pathTo.action.AtcotsDateConverter}" 
            converterMessage="your message here">
            <a:support event="onchange" ajaxSingle = "true" bypassUpdates="false">
            <f:param name="startHireDate" value="#{employeeVList.employeeV.startHireDate}"/>
            </a:support>
            </rich:calendar>
            </s:decorate>



            the converter never gets called,
            if I use it in page.xml:



            <param name="startHireDate" value="#{employeeVList.employeeV.startHireDate}"
             converterId="pathTo.action.MyDateConverter"/>



            it gets called twice for each component (this is with datePattern as MM/dd/yy and date entered as MM/dd/yyyy)



            17:58:04,138 DEBUG [MyDateConverter-getAsObject] string '01/01/2009' has been converted to date 'Thu Jan 01 00:00:00 EST 2009'
            17:58:04,138 DEBUG [MyDateConverter-getAsObject] string '01/01/2009' has been converted to date 'Thu Jan 01 00:00:00 EST 2009'







            this is with datePattern is MM/dd/yyyy and date entered manually as MM/dd/yy:



            18:23:28,593 DEBUG [AtcotsDateConverter-getAsObject] string '12/01/0009' has been converted to date 'Sun Dec 01 00:00:00 EST 9'
            18:23:28,593 DEBUG [AtcotsDateConverter-getAsObject] string '12/01/0009' has been converted to date 'Sun Dec 01 00:00:00 EST 9'



            So when I enter a date of 12/01/09 the date string is being padded by datePattern to 12/01/0009, before it even gets to the converter. 


            I added a couple of other formats to your dateConverter, to allow user to enter with dashes or dots, but if I enter in any format other than the 2 listed above, the converter never even gets called.

            • 3. Re: rich:calendar dateConverter and list sorting
              Tim Evers Master

              If you are using Seam then get rid of this in your faces-config.xml


              <converter>
              <converter-id>pathTo.action.MyDateConverter</converter-id>
              <converter-for-class>java.util.Date</converter-for-class>
              <converter-class>pathTo.action.MyDateConverter</converter-class>
              </converter>
              




              The point of the line


              @Converter(id = "GekkoDateConverter", forClass = Date.class)
              



              Is that you no longer need to worry about registering the converter in the xml file. And this also registers the converter as the default converter for all java.util.Date types.


              So in your component do NOT specify a converter. This converter should get used because the value property maps to a java.util.Date type.


              You shouldn't need to specify the converter in the pages.xml either.



              Let me know how you go with these changes. If it doesn't work could you post the source as you have it in your codebase now so I can see if I missed something.


              Thanks,
              Tim


              • 4. Re: rich:calendar dateConverter and list sorting
                gebuh Newbie

                Update, solved some stuff,
                The good:
                I have a workable solution, I have to use the converter tag in the rich:calendar component, use 2 digit year formats and use rich:messages to display any resultant converter errors


                The bad:
                The converter is not called for objects of type Date unless I specifically call it.  And some converter is being used, if I put the custom converter in the param tag in page.xml the date string is already preformatted before my converter gets to it.
                I have to use the rich:calendar converter tag in the component to get it to work correctly


                The ugly:
                Bad SimpleDateFormat, bad.  If you use a date with 2 digit year with a date format of 4 digit year SDF will convert it literally.  So when I looped through the acceptedFormats array it would use MM/dd/yyy to convert the date 12/01/09 to 12/01/0009.  So I removed all the 4 digit formats.


                So now I have this configuration:
                my component:



                <s:decorate id="afterDateHire" template="../layout/display.xhtml">
                <ui:define name="label"> Hire Date From:</ui:define>
                <rich:calendar id="afterDateHire" datePattern="MM/dd/yy"
                               value="#{employeeVList.employeeV.startHireDate}" enableManualInput = "true" 
                               converter="#{myDateConverter}" >
                <a:support event="onchange" ajaxSingle = "true" reRender = "afterDateHire" bypassUpdates="false" />     
                <f:param name="startHireDate" value="#{employeeVList.employeeV.startHireDate}"/>     
                </rich:calendar>
                </s:decorate>
                
                <rich:messages ajaxRendered="true" styleClass="errorMessage">
                </rich:messages>



                the messages tag is the only way I can get the failed messages to display on the page, if I don't use it I get FacesMessage(s) have been enqueued, but may not have been displayed messages in my server log.  I tried to use rich:message(it would be nice if the msg was next to the component) I got nothing, but I can live with this


                page.xml:



                <param name="hireDate" value="#{employeeVList.employeeV.hireDate}"/><param name="startHireDate" value="#{employeeVList.employeeV.startHireDate}"/>
                 <param name="endHireDate" value="#{employeeVList.employeeV.endHireDate}"/>




                this was where I was losing state during search, I had to undo some previous we didn't know what we were doing stuff.


                In Tim's converter I changed how messages are created - wouldn't work the other way for me, I kept getting NPE's for messages:



                public Object getAsObject(FacesContext context, UIComponent component,
                               String value) throws ConverterException {
                
                          
                          if (value == null || value.equals("")) { 
                            return null;
                        }
                        
                        List<String> acceptedFormats = new ArrayList<String>();
                        acceptedFormats.add("MM-dd-yy");
                        acceptedFormats.add("MM.dd.yy");
                        //Always check these 2 formats FIRST
                        acceptedFormats.add(0,"MM/dd/yy");
                        acceptedFormats.add(0,"MMddyy");
                        
                        for (String format : acceptedFormats) {
                            try {
                                 log.debug("dateString before parsing is " + value);
                                 Date parsedDate = AtcotsDateConverter.parseDate(value, format, "");
                                 log.debug("string '#0' has been converted to date '#1' using format '#2'", value, parsedDate, format);
                                 return parsedDate;
                            } catch (IllegalArgumentException e) {
                                 // don't do anything, loop thru all formats
                            }
                        }
                
                        String msgStr = value + " " + FAIL_MSG;
                        FacesMessage msg = new FacesMessage(msgStr);
                        throw new ConverterException(msg);
                        
                        
                     }
                



                Tim thanks again for the help, you definitely put me on the right path.
                I think that's it, hopefully my agony will save someone from a similar fate.   Now I'm off to try and add a validator to compare date components, somebody pray for me.

                • 5. Re: rich:calendar dateConverter and list sorting
                  Tim Evers Master

                  beth boose wrote on Dec 05, 2010 22:54:



                  The bad:
                  The converter is not called for objects of type Date unless I specifically call it.  And some converter is being used, if I put the custom converter in the param tag in page.xml the date string is already preformatted before my converter gets to it.
                  I have to use the rich:calendar converter tag in the component to get it to work correctly



                  Then you have something wrong in your configuration somewhere. Have you left other converters in your codebase that are mapped to convert the Dates?


                  Even if the line


                  @Converter(id = "GekkoDateConverter", forClass = Date.class)
                  



                  does not force the converter to be called then specifying the converter in the component's converter attribute most certainly should work. However, looking at your code I think you are not doing it right.


                  <rich:calendar 
                      id="afterDateHire"
                      datePattern="MM/dd/yy"
                      value="#{employeeVList.employeeV.startHireDate}" 
                      enableManualInput = "true" 
                      converter="#{myDateConverter}" >
                  



                  Looking at what you have posted here the EL expression #{myDateConverter} is going to be null.
                  That is of course going to mean it will use the default converter. Instead you should be using the id of the converter you want. NOT an instance of it. (Obviously you may have this mapped somewhere else but I can't see it :P)


                  So like this.


                  <rich:calendar 
                      id="afterDateHire"
                      datePattern="MM/dd/yy"
                      value="#{employeeVList.employeeV.startHireDate}" 
                      enableManualInput = "true" 
                      converter="pathTo.action.MyDateConverter" >
                  



                  If your converter isn't being called it means you have got it wrong. The converter attribute is used by many applications and is really quite a stable area. Sure there may be some components that may not be good at using the converters based on class types but specifying the converter like this should always work.


                  Please read this article about customer converters in JSF.



                  beth boose wrote on Dec 05, 2010 22:54:


                  The ugly:
                  Bad SimpleDateFormat, bad.  If you use a date with 2 digit year with a date format of 4 digit year SDF will convert it literally.  So when I looped through the acceptedFormats array it would use MM/dd/yyy to convert the date 12/01/09 to 12/01/0009.  So I removed all the 4 digit formats.



                  Well, you shouldn't be doing this. Like I clearly stated in my code. You MUST check the 2 digit year formats first. If there is a successful conversion from any of the 2 digit year formats then you do not check the 4 digit year formats. It is purely about the order you check the formats. The converter works fine if you check the 2 digit year formats first. ...Now that I look at my code maybe I didn't specifically state check all 2 digit year combos first but I figured you would work that out seeings as a 4 digit format will accept a 2 digit number and thus you don't want to check 4 digit formats until you are done checking all 2 digit year formats.



                  beth boose wrote on Dec 05, 2010 22:54:


                  the messages tag is the only way I can get the failed messages to display on the page, if I don't use it I get FacesMessage(s) have been enqueued, but may not have been displayed messages in my server log.  I tried to use rich:message(it would be nice if the msg was next to the component) I got nothing, but I can live with this

                  .....

                  In Tim's converter I changed how messages are created - wouldn't work the other way for me, I kept getting NPE's for messages:



                  Find out why you are getting the NPEs this will then fix your problem of the message not being beside the component. What I've done works, so what is the error you are getting?


                  I would highly recommend spending the time to get this working properly. The chances of this being the last converter you write is pretty low.


                  Also, I would basically tell you now to give up on the idea of a Validator to compare dates in 2 different components unless you are using JSF 2. It has been tried many times and it is not easy and there is no solution that actually works for all scenarios. Instead you should validate the dependency between fields inside whatever action method uses these values.

                  • 6. Re: rich:calendar dateConverter and list sorting
                    gebuh Newbie

                    Tim Evers wrote on Dec 05, 2010 23:25:


                    Then you have something wrong in your configuration somewhere. Have you left other converters in your codebase that are mapped to convert the Dates?

                    Even if the line

                    @Converter(id = "GekkoDateConverter", forClass = Date.class)
                    



                    does not force the converter to be called then specifying the converter in the component's converter attribute most certainly should work. However, looking at your code I think you are not doing it right.


                    That's entirely possible, but I haven't been able to find whatever it might be.  I can call the custom converter using the converter tag in rich:calendar, so this actually works:
                    converter="#{myDateConverter}"
                    this does not:
                    converter="pathTo.action.MyDateConverter"


                    I'll continue trying to figure out why it doesn't work without explicitly using the tag, adding them to all of our calendar components was painful.




                    Well, you shouldn't be doing this. Like I clearly stated in my code. You MUST check the 2 digit year formats first. If there is a successful conversion from any of the 2 digit year formats then you do not check the 4 digit year formats. It is purely about the order you check the formats. The converter works fine if you check the 2 digit year formats first. ...Now that I look at my code maybe I didn't specifically state check all 2 digit year combos first but I figured you would work that out seeings as a 4 digit format will accept a 2 digit number and thus you don't want to check 4 digit formats until you are done checking all 2 digit year formats.


                    You're right, I wasn't paying attention, but I found that while a 4 digit pattern won't correctly format a 2 digit year, a 2 digit pattern will format a 4 digit year correctly, so I went with that.



                    Find out why you are getting the NPEs this will then fix your problem of the message not being beside the component. What I've done works, so what is the error you are getting?



                    The NPE's was caused by this:
                    MessageFactory.getMessage
                    Seam blows up when the message is null, another configuration issue?
                    when I used:
                    FacesMessage msg = new FacesMessage(msgStr);


                    the messages are collected correctly.  The problem with displaying the messages was in layout/display.xhtml, we have the default and once I added msessage tags to it:



                    <div class="prop">
                            <s:label styleClass="name #{invalid?'errors':''}">
                                <ui:insert name="label"/>
                                <s:span styleClass="required" rendered="#{required}">*</s:span>
                            </s:label>
                            <span class="value #{invalid?'errors':''}">
                                <s:validateAll>
                                    <ui:insert/>
                                </s:validateAll>
                            </span>
                            <span class="error">
                                <h:graphicImage value="/img/error.gif" rendered="#{invalid}" styleClass="errors"/>
                                <s:message styleClass="errors"/>
                            </span>
                        </div>



                    The messages displayed where and how they were supposed to.



                    I would highly recommend spending the time to get this working properly. The chances of this being the last converter you write is pretty low.

                    Also, I would basically tell you now to give up on the idea of a Validator to compare dates in 2 different components unless you are using JSF 2. It has been tried many times and it is not easy and there is no solution that actually works for all scenarios. Instead you should validate the dependency between fields inside whatever action method uses these values.


                    Will do, thanx for the advice.