How to set converter properties for each row/item of h:dataTable/ui:repeat?
Asked Answered
A

2

14

I have created a custom ISO date time Converter:

public class IsoDateTimeConverter implements Converter, StateHolder {

    private Class type;
    private String pattern;

    private boolean transientValue = false;

    public void setType(Class type) {
        this.type = type;
    }

    public void setPattern(String pattern) {
        this.pattern = pattern;
    }

    @Override
    public Object getAsObject(FacesContext context, UIComponent component, String value) throws ConverterException {
        if (StringCheck.isNullOrEmpty(value)) {
            throw new ConverterException("value not specified");
        }

        try {
            if (IsoDate.class.equals(type)) {

                if (WebConst.ISO_DATE_NONE.equals(value)) {
                    return IsoDate.DUMMY;
                } else {
                    //TODO User spezifische TimeZone auslesen
                    return new IsoDate(value, TimeZone.getDefault().getID());
                }

            } else if (IsoTime.class.equals(type)) {

                if (WebConst.ISO_TIME_NONE.equals(value)) {
                    return IsoTime.DUMMY;
                } else {
                    //TODO User spezifische TimeZone auslesen
                    return new IsoTime(value, TimeZone.getDefault().getID());
                }

            } else if (IsoTimestamp.class.equals(type)) {

                if (WebConst.ISO_TIMESTAMP_NONE.equals(value)) {
                    return IsoTimestamp.DUMMY;
                } else {
                    //TODO User spezifische TimeZone auslesen
                    return new IsoTimestamp(value, TimeZone.getDefault().getID());
                }

            } else {
                throw new ConverterException("value not convertible");
            }
        } catch (Exception e) {
            throw new ConverterException(e.getMessage());
        }
    }

    @Override
    public String getAsString(FacesContext context, UIComponent component, Object value) throws ConverterException {
        if (value == null) {
            throw new ConverterException("value not specified");
        }

        if (IsoDate.class.equals(value)) {
            IsoDate isoDate = (IsoDate) value;

            if (isoDate.isDummy()) {
                return WebConst.ISO_DATE_NONE;
            } else {
                //TODO User spezifische TimeZone auslesen
                return isoDate.toString(pattern, TimeZone.getDefault().getID(), false);
            }

        } else if (IsoTime.class.equals(value)) {
            IsoTime isoTime = (IsoTime) value;

            if (isoTime.isDummy()) {
                return WebConst.ISO_TIME_NONE;
            } else {
                //TODO User spezifische TimeZone auslesen
                return isoTime.toString(pattern, TimeZone.getDefault().getID(), false);
            }

        } else if (IsoTimestamp.class.equals(value)) {
            IsoTimestamp isoTimestamp = (IsoTimestamp) value;

            if (isoTimestamp.isDummy()) {
                return WebConst.ISO_TIMESTAMP_NONE;
            } else {
                //TODO User spezifische TimeZone auslesen
                return isoTimestamp.toString(pattern, TimeZone.getDefault().getID(), false);
            }

        } else {
            throw new ConverterException("value not convertible");
        }
    }

    @Override
    public Object saveState(FacesContext context) {
        return new Object[]{type, pattern};
    }

    @Override
    public void restoreState(FacesContext context, Object state) {
        type = (Class) ((Object[]) state)[0];
        pattern = (String) ((Object[]) state)[1];
    }

    @Override
    public boolean isTransient() {
        return transientValue;
    }

    @Override
    public void setTransient(boolean transientValue) {
        this.transientValue = transientValue;
    }
}

And I use the Converter as <mh:IsoDateTimeConverter> in the following view:

<p:dataTable value="#{imports.list}" var="item">
    <p:column>
        <h:outputText value="#{item.balanceDate}" immediate="true">
            <mh:IsoDateTimeConverter type="#{webConst.ISO_DATE_CLASS}" pattern="#{webConst.ISO_DATE_FORMAT}"/>
        </h:outputText>
    </p:column>
</p:dataTable>

The problem is, when I first open this view, all properties are set in my Converter class only once and then the datatable renders and converts the values based on initial properties.

I expected that the properties are set on a per-row basis. How can I achieve this?

Anglaangle answered 23/9, 2011 at 14:26 Comment(0)
W
27

To the point, you expected that the converter's properties are set every time a datatable row is rendered. This is indeed not true. JSF will create only one converter instance per component when the view is to be built, it will not create/reset the converter each time the row is rendered.

There are several ways to get it to work.

  • Pass the dynamic attributes as <f:attribute> of the component and let the Converter intercept on that. You can find an example here: JSF convertDateTime with timezone in datatable. This can then be used as

    <h:outputText value="#{item.balanceDate}">
        <f:converter converterId="isoDateTimeConverter" />
        <f:attribute name="pattern" value="#{item.pattern}" />
    </h:outputText>
    

  • Use an EL function instead of a Converter. You can find an example here: Facelets and JSTL (Converting a Date to a String for use in a field). This can then be used as

    <h:outputText value="#{mh:convertIsoDate(item.balanceDate, item.pattern)}" />
    

  • Bind the converter and datatable's DataModel as a property of the same managed bean. This way you will be able to set the converter's properties based on the row data before returning it. Here's a basic kickoff example based on standard JSF components and standard DateTimeConverter (it should work equally good on PrimeFaces components and with your custom converter):

    <h:dataTable value="#{bean.model}" var="item">
        <h:column>
            <h:outputText value="#{item.date}" converter="#{bean.converter}" />
        </h:column>
    </h:dataTable>
    

    with

    @ManagedBean
    @ViewScoped
    public class Bean implements Serializable {
    
        private List<Item> items;
        private DataModel<Item> model;
        private DateTimeConverter converter;
    
        @PostConstruct
        public void init() {
            items = Arrays.asList(
                new Item(new Date(), "dd-MM-yyyy"), 
                new Item(new Date(), "yyyy-MM-dd"), 
                new Item(new Date(), "MM/dd/yyyy"));
            model = new ListDataModel<Item>(items);
            converter = new DateTimeConverter();
        }
    
        public DataModel<Item> getModel() {
            return model;
        }
    
        public Converter getConverter() {
            converter.setPattern(model.getRowData().getPattern());
            return converter;
        }
    
    }
    

    (the Item class is just a bean with two properties Date date and String pattern)

    this results in

    23-09-2011
    2011-09-23
    09/23/2011


  • Use OmniFaces <o:converter> instead. It supports render time evaluation of EL in the attributes. See also the <o:converter> showcase example.

    <h:outputText value="#{item.balanceDate}">
        <o:converter converterId="isoDateTimeConverter" pattern="#{item.pattern}" />
    </h:outputText>
    
Wilmawilmar answered 23/9, 2011 at 15:19 Comment(1)
I use f:convertNumber (which matches to your f:* pattern) and a dynamic attribute currencyCode="#{rowVar.someCurrencyCode}" by someCurrencyCode is a property of an entity and rowVar comes from p:dataTable var="rowVar". So I have to write my own converter here then? I already have Primefaces 6.2, if that helps. Or the Omnifaces thing looks pretty straight-forward.Halcomb
F
1

The above excellent (as always) answer from BalusC is comprehensive but didn't quite hit my exact requirement. In my case, I need to bind a Converter to each iteration in a ui:repeat. I need a different Converter depending on each item being repeated. The answer did point me in the right direction, though, so I thought it worth sharing my solution in case it helps anyone else.

I use a Converter that delegates all it's work to another Converter object specified in the attribute, as in the first of BalusC's answers. Note that this doesn't help at all if you wish to use converters with parameters, it's aimed at the situation where you would want to bind a Converter to a property of a repeating object.

Here's the delegating Converter. It's also a Validator, which works in exactly the same way.

// package and imports omitted for brevity
@FacesConverter(value="delegatingConverter")
@FacesValidator(value="delegatingValidator")
public class Delegator implements Converter, Validator {

    // Constants ---------------------------------------------------------------
    private static final String CONVERTER_ATTRIBUTE_NAME = "delegateConverter";
    private static final String VALIDATOR_ATTRIBUTE_NAME = "delegateValidator";

    // Business Methods --------------------------------------------------------
    @Override
    public Object getAsObject(FacesContext context, UIComponent component, 
            String value) throws ConverterException {
        return retrieveDelegate(component, Converter.class, CONVERTER_ATTRIBUTE_NAME)
                .getAsObject(context, component, value);
    }

    @Override
    public String getAsString(FacesContext context, UIComponent component, 
            Object value) throws ConverterException {
        return retrieveDelegate(component, Converter.class, CONVERTER_ATTRIBUTE_NAME)
                .getAsString(context, component, value);
    }

    @Override
    public void validate(FacesContext context, UIComponent component, 
            Object value) throws ValidatorException {
        retrieveDelegate(component, Validator.class, VALIDATOR_ATTRIBUTE_NAME)
                .validate(context, component, value);
    }
    
    private <T> T retrieveDelegate(UIComponent component, Class<T> clazz,
            String attributeName) {
        Object delegate = component.getAttributes().get(attributeName);
        if (delegate == null) {
            throw new UnsupportedOperationException("No delegate was specified."
                    + "  To specify, use an f:attribute tag with: name=\"" 
                    + attributeName + "\"");
        }
        if (!(clazz.isAssignableFrom(delegate.getClass()))) {
            throw new UnsupportedOperationException("The specified delegate "
                    + "was not a " + clazz.getSimpleName() + " object.  " +
                    "Delegate was: " + delegate.getClass().getName());
        }
        return (T) delegate;
    }
}

So now where I would wish to use this code within my ui:repeat, which won't work:

<h:outputText value="#{item.balanceDate}">
    <f:converter binding="#{item.converter} />
    <f:validator binding="#{item.validator} />
</h:outputText>

I can instead use this code, which works OK:

<h:outputText value="#{item.balanceDate}">
  <f:converter converterId="delegatingConverter"/>
  <f:validator validatorId="delegatingValidator"/>
  <f:attribute name="delegateConverter" value="#{item.converter}"/>
  <f:attribute name="delegateValidator" value="#{item.validator}"/>
</h:outputText>

Assuming that the repeating item has a public Converter getConverter() method and similar for the Validator.

This does have the advantage that the same Converters or Validators that are used elsewhere can be re-used without any changes.

Fontanel answered 13/7, 2020 at 14:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.