Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cross field validation in jsf h:datatable using p:calendar

I noticed this question was asked, but it has not been answered correctly.

I have a datatable that has two columns start date and end date. Both contain primefaces p:calendar controls in them. I need to ensure that for each row that the date in column1 is not after the date in column2. I would like to tie this into the JSF validation framework, but I'm having trouble.

i've tried marking the datatable rowStatePreserved="true" , this allows me to get the values, but something is still wrong as when it fails, all the values in the first row overwrite all the other values. What am I doing wrong, or should I be using a completely different strategy?

xhtml code

    <h:form>
 <f:event type="postValidate" listener="#{bean.doCrossFieldValidation}"/>
       <p:dataTable id="eventDaysTable" value="#{course.courseSchedules}" var="_eventDay" styleClass="compactDataTable"
                                 >
                        <p:column id="eventDayStartColumn">
                            <f:facet name="header">
                                Start
                            </f:facet>
                            <p:calendar id="startDate" required="true"  value="#{_eventDay.startTime}" pattern="MM/dd/yyyy hh:mm a"/>
                        </p:column>
                        <p:column id="eventDayEndColumn">
                            <f:facet name="header">
                                End
                            </f:facet>
                            <p:calendar id="endDate" required="true"  value="#{_eventDay.endTime}" pattern="MM/dd/yyyy hh:mm a"/>
                        </p:column>                                        
                    </p:dataTable>
        </h:form>

validationCode

 public void doCrossFieldValidation(ComponentSystemEvent cse) {


        UIData eventsDaysStable = (UIData) cse.getComponent().findComponent("eventDaysTable");

        if (null != eventsDaysStable && eventsDaysStable.isRendered()) {

            Iterator<UIComponent> startDateCalendarIterator = eventsDaysStable.findComponent("eventDayStartColumn").getChildren().iterator();
            Iterator<UIComponent> endDateCalendarIterator = eventsDaysStable.findComponent("eventDayEndColumn").getChildren().iterator();

            while (startDateCalendarIterator.hasNext() && endDateCalendarIterator.hasNext()) {
                org.primefaces.component.calendar.Calendar startDateComponent = (org.primefaces.component.calendar.Calendar) startDateCalendarIterator.next();
                org.primefaces.component.calendar.Calendar endDateComponent = (org.primefaces.component.calendar.Calendar) endDateCalendarIterator.next();

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


                if (null != startDate && null != endDate && startDate.after(endDate)) {
                    eventScheduleChronologyOk = false;
                    startDateComponent.setValid(false);
                    endDateComponent.setValid(false);
                }

            }

            if (!eventScheduleChronologyOk) {
                showErrorMessage(ProductManagementMessage.PRODUCT_SCHEDULE_OUT_OF_ORDER);
            }

        }

    }
like image 611
Matt Broekhuis Avatar asked Jun 21 '12 21:06

Matt Broekhuis


1 Answers

What am I doing wrong

Performing validation outside the context of the datatable in a non-standard JSF way. The row data is only available while you're (or JSF is) iterating over the datatable. There's physically only one <p:calendar> component in every column which has multiple different states depending on the current datatable iteration round. Those states are not available when you're not iterating over the datatable. You'll only get null as value then.

Technically, with your different validation approach so far, you should be invoking visitTree() method on the UIData component and performing the job in the VisitCallback implementation. This will iterate over the datatable.

For example,

dataTable.visitTree(VisitContext.createVisitContext(), new VisitCallback() {
    @Override
    public VisitResult visit(VisitContext context, UIComponent component) {
        // Check if component is instance of <p:calendar> and collect its value by its ID.

        return VisitResult.ACCEPT;
    }
});

This is only clumsy. This gives you every single row, you'd need to maintain and check the row index youself and collect the values. Note that calling UIInput#setValid() should also be done inside the VisitCallback implementation.


or should I be using a completely different strategy?

Yes, use a normal Validator the standard JSF way. You can pass the one component as an attribute of the other component.

E.g.

<p:column>
    <p:calendar binding="#{startDateComponent}" id="startDate" required="true"  value="#{item.start}" pattern="MM/dd/yyyy hh:mm a"/>
</p:column>
<p:column >
    <p:calendar id="endDate" required="true"  value="#{item.end}" pattern="MM/dd/yyyy hh:mm a">
        <f:validator validatorId="dateRangeValidator" />
        <f:attribute name="startDateComponent" value="#{startDateComponent}" />
    </p:calendar>
</p:column>                                        

with

@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));
        }
    }

}

As both components are in the same row, the startDateComponent will "automagically" give the right value back on getValue() everytime this validator is invoked. Note that this validator is also reuseable outside the datatable while your initial approach isn't.

Alternatively, you can use OmniFaces <o:validateOrder> as a complete solution. Its showcase example even shows off this specific use case of <p:calendar> components inside a <p:dataTable>.

like image 143
BalusC Avatar answered Oct 07 '22 17:10

BalusC