Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Replace null or empty strings with a specified value while outputting using a JSF converter

The following is a converter basically intended to trim leading and trailing white spaces and replace more than one space between words in a sentence or text with a single space. The converter is now modified to replace null or empty strings with "Not available" (may be localized dynamically, if needed).

@FacesConverter(forClass = String.class)
public class StringTrimmer implements Converter {

    @Override
    public Object getAsObject(FacesContext context, UIComponent component, String value) {
        return Boolean.TRUE.equals(component.getAttributes().get("skipConverter")) ? value : value == null ? null : value.trim().replaceAll("\\s+", " ");
    }

    @Override
    public String getAsString(FacesContext context, UIComponent component, Object value) {
        return Boolean.TRUE.equals(component.getAttributes().get("skipConverter")) ? value == null ? null : value.toString() : value == null || ((String) value).trim().length() == 0 ? "Not available" : ((String) value).trim().replaceAll("\\s+", " ");
    }
}

Since converters are not invoked, when a model value is null based on the previous question, com.sun.faces.renderkit.html_basic.TextRenderer has been extended with the intention of invoking converters, when a property value in the associated model is null.

public final class HtmlBasicRenderer extends TextRenderer {

    @Override
    public String getCurrentValue(FacesContext context, UIComponent component) {

        if (component instanceof UIInput) {
            Object submittedValue = ((UIInput) component).getSubmittedValue();

            if (submittedValue != null) {
                return submittedValue.toString();
            }
        }

        return getFormattedValue(context, component, getValue(component));
    }
}

The following conditional test has been removed so that the getFormattedValue() method can be invoked, even if a null value is encountered.

Object currentObj = getValue(component);

if (currentObj != null) {
    currentValue = getFormattedValue(context, component, currentObj);
}

This has been registered in faces-config.xml as follows.

<render-kit>
    <renderer>
        <component-family>javax.faces.Output</component-family>
        <renderer-type>javax.faces.Text</renderer-type>
        <renderer-class>com.example.renderer.HtmlBasicRenderer</renderer-class>
    </renderer>
</render-kit>

The converter StringTrimmer is still not invoked (getAsString()), when a property value in the target model returns null.

Putting a conditional test in EL like #{empty bean.value ? 'Not available' : bean.value} everywhere throughout the application is insanity. Any suggestion?

It is Mojarra 2.2.12.


Update :

Converted values are available, when one of the return statements inside the getFormattedValue() method returning an empty string "", when currentValue is null, is modified to return a converted value in call to

javax.​faces.​convert.​Converter.getAsString(FacesContext context, UIComponent component, Object value)

inside that method getFormattedValue().

Thus, the following,

if(currentValue == null) {
    return "";
}

needs to be replaced with,

if (currentValue == null) {
    converter = Util.getConverterForClass("".getClass(), context);
    return converter == null ? "" : converter.getAsString(context, component, currentValue);
}

(Needs suggestions).

like image 343
Tiny Avatar asked Feb 17 '16 12:02

Tiny


1 Answers

In first place, a Converter is never intented to enforce a "default value".

Regardless of the question, whatever you do in the getAsString(), you should guarantee that the resulting String can be converted back to the original Object when you pass it back through getAsObject(). Your converter doesn't do that. Even though you'd unlikely ever use it, technically the converter needs to be modified to convert the exact string "Not available" back to null. In other words, your converter must be designed in such way that getAsObject() and getAsString() can successfully pass each other's result in an infinite loop and give the same result back every time.

As to the concrete functional requirement of enforcing a default value, instead of in a Converter you should do so in either the model or the view, depending on where the actual default value is coming from. In your specific case, you just want to have a default placeholder text/label in the user interface when there's no such value. This belongs in the view.

Putting a conditional test in EL like #{empty bean.value ? 'Not available' : bean.value} everywhere throughout the application is insanity.

I fully agree that this is insanity if you're already like halfway the application development and have hundreds of them over all place. It is however not insanity if you already took that into account from beginning on. If you didn't, then well, learn the lesson, bite the bullet and routinely fix the code accordingly. Decent IDEs have regex based find&replace which could aid in this. Everyone has been there and done that kind of insanity, even myself. To reduce boilerplate, wrap it in an EL function or tagfile.

As to the concrete problem of the Converter not being invoked when the model value is null, which I personally fully agree to be unexpected behavior, this was ever reported as Mojarra issue 630. This is later closed as WONTFIX because it caused test case failures and is after all better to be reported as a JSF specification issue. This was done as JSF spec issue 475 (during JSF 1.2 already). I checked MyFaces 2.2.9 on this and it also don't trigger the converter and thus exposes the same spec issue.

The technical problem, however, is understandable. A null value doesn't have a sensible getClass(), so the converter can't be looked up by value's class this way. It works only when the converter is explicitly registered on the component.

<h:outputText value="#{bean.potentiallyNullValue}" converter="stringTrimmer" />

It should work when the value is an empty string "", which is relatively trivial to implement in getValue() of the custom renderer extending Mojarra's TextRenderer.

@Override
protected Object getValue(UIComponent component) {
    Object value = super.getValue(component);
    return (value != null) ? value : "";
}

However, when I tried that myself here, it still failed. What turns out, the converter-by-class is wholly skipped when the value is an instance of String. It still works only when the converter is explicitly registered on the component. This is most likely an oversight during implementing spec issue 131 for JSF 1.2 (before this version, converters for String.class were not supported at all and this issue only fixed it for decode, not for encode).

This can be overriden in the same custom renderer (with the above getValue() override) with below override of getFormattedValue() whereby the converter is explicitly looked up.

@Override
protected String getFormattedValue(FacesContext context, UIComponent component, Object currentValue) throws ConverterException {
    Converter converter = ((UIOutput) component).getConverter();

    if (converter == null) {
        converter = context.getApplication().createConverter(currentValue.getClass());
    }

    return super.getFormattedValue(context, component, "".equals(currentValue) ? null : currentValue, converter);
}

Note that you don't need to check for UIInput as you've explicitly registered your custom renderer on javax.faces.Output/javax.faces.Text component family/type only (i.e. it will only run on <h:outputText> components).

Nonetheless, best solution is still to create an EL function or tagfile for this.

<h:outputText value="#{empty bean.value ? 'Not available' : bean.value}" />
<h:outputText value="#{of:coalesce(bean.value, 'Not Available')}" />
<h:outputText value="#{of:coalesce(bean.value, i18n.na)}" />
<h:outputText value="#{your:value(bean.value)}" />
<your:text value="#{bean.value}" />
like image 69
BalusC Avatar answered Nov 13 '22 02:11

BalusC