17
votes

I want to compare two dates (StartDate and EndDate) and check whether one is before the other. The simplest solution is to just do it on the backing bean and "short-circuit" the method.

However this validation does not happen concurrently with the other form validations. For example if I have another field that requires validation (besides the dates) and has invalid input, I will only get a message for that specific field. Only if the other fields are valid will I get the date validation based on the backing bean.

Anyone have a solution?

4
possible duplicate of JSF validate on submit formskuntsel

4 Answers

36
votes

However this validation does not happen concurrently with the other form validations.

A backing bean action method is indeed not intented to perform input validation.


Anyone have a solution?

Use the right tool for the job; use a normal Validator.

@FacesValidator("dataRangeValidator")
public class DateRangeValidator implements Validator {
    // ...
}

Validating multiple input values with a single validator is in turn however indeed a story apart. Basically, you should be grabbing/passing the other component or its value along into the validate() method implementation. In its simplest form, you could use <f:attribute> for this. Assuming that you're using <p:calendar> to pick dates, here's a concrete kickoff example:

<p:calendar id="startDate" binding="#{startDateComponent}" value="#{bean.startDate}" pattern="MM/dd/yyyy" required="true" />
<p:calendar id="endDate" value="#{bean.endDate}" pattern="MM/dd/yyyy" required="true">
    <f:validator validatorId="dateRangeValidator" />
    <f:attribute name="startDateComponent" value="#{startDateComponent}" />
</p:calendar>

(note the binding attribute, it makes the component available in the EL scope on exactly the given variable name; also note that this example is as-is and that you should absolutely not bind it to a bean property!)

Where the dateRangeValidator look like this:

@FacesValidator("dateRangeValidator")
public class DateRangeValidator implements Validator {

    @Override
    public void validate(FacesContext context, UIComponent component, Object value) throws ValidatorException {
        if (value == null) {
            return; // Let required="true" handle.
        }

        UIInput startDateComponent = (UIInput) component.getAttributes().get("startDateComponent");

        if (!startDateComponent.isValid()) {
            return; // Already invalidated. Don't care about it then.
        }

        Date startDate = (Date) startDateComponent.getValue();

        if (startDate == null) {
            return; // Let required="true" handle.
        }

        Date endDate = (Date) value;

        if (startDate.after(endDate)) {
            startDateComponent.setValid(false);
            throw new ValidatorException(new FacesMessage(
                FacesMessage.SEVERITY_ERROR, "Start date may not be after end date.", null));
        }
    }

}

If you happen to use JSF utility library OmniFaces, then you could also just use its <o:validateOrder> component. The requirement can be achieved as follows without the need for a custom validator:

<p:calendar id="startDate" value="#{bean.startDate}" pattern="MM/dd/yyyy" required="true" />
<p:calendar id="endDate" value="#{bean.endDate}" pattern="MM/dd/yyyy" required="true" />
<o:validateOrder components="startDate endDate" />

See also:

6
votes

if you're using PrimeFaces which can limit a minimum and maximum dates. the user could not choose a greater range this is an example:

<p:calendar id="startDate" value="#{bean.startDate}" maxdate="#{bean.endDate}">
     <p:ajax event="dateSelect" update="endDate"/>
</p:calendar>
<p:calendar id="endDate" value="#{bean.endDate}" mindate="#{bean.startDate}" disabled="#{empty bean.startDate}">
      <p:ajax event="dateSelect" update="startDate"/>
 </p:calendar>
3
votes

Taking the Answer BalusC like Base, and for me use in the future...

This can to set the Interval (Year, Month, Week), the Reference Comparation Initial, or Final.

@FacesValidator("dateRangeValidator")
public class DateRangeValidator implements Validator {

   @Override
   public void validate(FacesContext facesContext, UIComponent component,
           Object value) throws ValidatorException {

       UIInput dateIniComponent = (UIInput) component.getAttributes().get("dateIniComponent");
       UIInput dateFinComponent = (UIInput) component.getAttributes().get("dateFinComponent");
       String range = ((String) component.getAttributes().get("range")).toLowerCase();
       String reference = ((String) component.getAttributes().get("reference")).toLowerCase();

       if (value == null) {
           return;
       } else if (value instanceof Date) {
           Date dateIni = null;
           Date dateFin = null;
           if ((dateIniComponent == null) && (dateFinComponent != null)) {
               if (!dateFinComponent.isValid()) {
                   return; //No hay datos contra quien comparar
               }
               dateFin = (Date) dateFinComponent.getValue();
               dateIni = (Date) value;
           }

           if ((dateFinComponent == null) && (dateIniComponent != null)) {
               if (!dateIniComponent.isValid()) {
                   return; //No hay datos contra quien comparar
               }
               dateIni = (Date) dateIniComponent.getValue();
               dateFin = (Date) value;
           }

           if ((dateIni != null) && (dateFin != null)) {
               Calendar cal = Calendar.getInstance();

               cal.setTime(dateIni);
               Integer yearIni = cal.get(Calendar.YEAR);
               Integer monthIni = cal.get(Calendar.MONTH);
               Long daysMonthIni = (long) YearMonth.of(yearIni, monthIni + 1).lengthOfMonth();
               Long daysYearIni = (long) cal.getActualMaximum(Calendar.DAY_OF_YEAR);

               cal.setTime(dateFin);
               Integer yearFin = cal.get(Calendar.YEAR);
               Integer monthFin = cal.get(Calendar.MONTH);
               Long daysMonthFin = (long) YearMonth.of(yearFin, monthFin + 1).lengthOfMonth();
               Long daysYearFin = (long) cal.getActualMaximum(Calendar.DAY_OF_YEAR);

               Long daysAllowed =
                       ("year".equals(range) ? ("ini".equals(reference)?daysYearIni:("fin".equals(reference)?daysYearFin:null)) :
                       ("month".equals(range) ? ("ini".equals(reference)?daysMonthIni:("fin".equals(reference)?daysMonthFin:null)) : 
                       ("week".equals(range) ? 7 : null)));

               Long daysBetweenDates = TimeUnit.DAYS.convert(dateFin.getTime() - dateIni.getTime(), TimeUnit.MILLISECONDS);

               if (daysAllowed == null) {
                   FacesMessage facesMessage
                           = new FacesMessage(
                                   FacesMessage.SEVERITY_ERROR,
                                   "Rango de fechas mal expresado en el facelet (vista) ",
                                   "Rango de fechas mal expresado en el facelet (vista) ");
                   throw new ValidatorException(facesMessage);
               }

               if (dateFin.before(dateIni)) {
                   FacesMessage facesMessage
                           = new FacesMessage(
                                   FacesMessage.SEVERITY_ERROR,
                                   "Fecha Final No es posterior a Fecha Inicial ",
                                   "La Fecha Final debe ser posterior a Fecha Inicial");
                   throw new ValidatorException(facesMessage);
               }

               if (daysBetweenDates > daysAllowed) {
                   FacesMessage facesMessage
                           = new FacesMessage(
                                   FacesMessage.SEVERITY_ERROR,
                                   "Se ha excedido los dias permitidos " + daysAllowed + " entre fechas seleccionadas, entre las fechas hay " + daysBetweenDates + " dias",
                                   "entre las fechas hay " + daysBetweenDates + " dias");
                   throw new ValidatorException(facesMessage);
               }
           }

       }
   }

}

Now in the view

    <p:outputLabel value="Date Initial:" for="itHeadDateInitial" />
    <p:calendar id="itHeadDateInitial"
                navigator="true" 
                binding="#{bindingDateIniComponent}" 
                value="#{theBean.DateIni}"
                pattern="dd-MM-yyyy" mask="true" >
        <f:validator validatorId="dateRangeValidator" />
        <f:attribute name="dateFinComponent" value="#{bindingDateFinComponent}" />
        <f:attribute name="range" value="year" />
        <f:attribute name="reference" value="ini" />
    </p:calendar>


    <p:outputLabel value="Date Final:" for="itHeadDateFinal" />
    <p:calendar id="itHeadDateFinal"
                navigator="true" 
                binding="#{bindingDateFinComponent}" 
                value="#{theBean.DateFin}"
                pattern="dd-MM-yyyy" mask="true" >
        <f:validator validatorId="dateRangeValidator" />
        <f:attribute name="dateIniComponent" value="#{bindingDateIniComponent}" />
        <f:attribute name="range" value="year" />
        <f:attribute name="reference" value="ini" />
    </p:calendar>
2
votes

As BalusC solution works only if you have one date range to validate on the form, here is an improvement to allow multiple date range validations:

  • add another <f:attribute> to the endDate calendar component where you specify the binding attribute name to startDate component:

    <f:attribute name="bindingAttributeName" value="startDateComponent" />
    
  • then in the validator:

    String startDateBindingAttrName = (String) component.getAttributes().get("bindingAttributeName");
    UIInput startDateComponent = (UIInput) component.getAttributes().get(startDateBindingAttrName);