Translate

вівторок, 18 січня 2011 р.

Hacking the enum conversion in JSF 1.2_x


Сьогодні зіткнувся з тим що JSF 1.2 неправильно працює з енумами
Трохи пошукав і знайшов ось тут рішеня і навіть дет
Suppose you have an enum and some of its methods are overridden by concrete enum values:
public enum SomeEnum {
    SOME_VALUE,
    OTHER_VALUE {
        @Override
        public boolean isDifferent() {
            return true;
        }
    };
 
    public boolean isDifferent() {
        return false;
    }
}
In the example a custom isDifferent() method is overridden, but it could as well be the existing toString().
If you attempt to use such an Enum (the OTHER_VALUE to be precise) within a view in a JSF application (using the reference implementation, haven’t tried MyFaces), you’re likely to get an exception like this:
javax.faces.convert.ConverterException: 'OTHER_VALUE' must be convertible
    to an enum from the enum, but no enum class provided.
After some debugging it turns out that enum values with non-empty body are not handled properly. If you have a look at the com.sun.faces.application.ApplicationImpl class, there’s a method
public Converter createConverter(Class targetClass) {
    ...
}
Normally, when JSF is looking for a converter for an enum without overridden values, the targetClass parameter is the concrete enum class (it should be SomeEnum in our case, but it actually is SomeEnum$Something). The method first tries to lookup a base type converter, but there’s none for SomeEnum$Something (that’s a surprise!). So, in the next attempt, it searches for interfaces implemented by the targetClass – but nothing here again. As a last resort, the method goes through a collection of targetClass’s superclasses. This time a superclass is found – it’s the SomeEnum – so the
protected Converter createConverterBasedOnClass(
            Class targetClass, Class baseClass) {
    ...
}
method is called with SomeEnum as the targetClass and SomeEnum$Something as the baseClass. Similar steps as above are now taken to find and appropriate converter for interfaces/superclasses of SomeEnum. As SomeEnum extends java.lang.Enum, the method is called recursively with null as the baseClass:
createConverterBasedOnClass(java.lang.Enum, null);
Finally, a matching converter is found for java.lang.Enum and an instance of javax.faces.convert.EnumConverter is created. Almost everything is fine, except for the fact that it gets the above null (passed as the baseClass above) injected as the target class in constructor parameter.
This causes the converter to throw the exception mentioned above when assuring that its target class is not null:
if (targetClass == null) {
    throw new ConverterException(MessageFactory.getMessage(
              context,
              ENUM_NO_CLASS_ID,
              value,
              MessageFactory.getLabel(context, component))
    );
}
Unlike the problem description, the solution seems to be quite simple. What we basically need to do is to handle the case when JSF is looking for a converter for an anonymous inner class of an enum and swap the classes in the call to createConverterBasedOnClass(). So if the tagetClass is our enum and the baseClass is its anonymous extension, we should call:
createConverterBasedOnClass(Enum.class, targetClass);
otherwise we shouldn’t change anything and call:
createConverterBasedOnClass(targetClass, baseClass);
To weave our solution into JSF we need to extend the com.sun.faces.application.ApplicationImpl class and override its createConverterBasedOnClass() method:
package org.kunicki.jsf;
 
import com.sun.faces.application.ApplicationImpl;
 
import javax.faces.convert.Converter;
 
/**
 * @author jacek@kunicki.org
 */
public class FixedEnumConverterApplicationImpl
    extends ApplicationImpl {
 
    @Override
    protected Converter createConverterBasedOnClass(
            Class targetClass, Class baseClass) {
        // if base class is an anonymous  inner class of an
        // enum, use Enum as the target class and targetClass
        // as the base class
        if (targetClass.isEnum()
                && baseClass.getName().contains(
                      targetClass.getName())
                && baseClass.getName().contains("$")) {
            return super.createConverterBasedOnClass(
                      Enum.class, targetClass);
        } else {
            return super.createConverterBasedOnClass(
                      targetClass, baseClass);
        }
    }
}
Now we need make JSF use the above implementation instead of the standard one. This is done by extending the com.sun.faces.application.ApplicationFactoryImpl and override the getApplication() method to simply return our FixedEnumConverterApplicationImpl:
package org.kunicki.jsf;
 
import com.sun.faces.application.ApplicationFactoryImpl;
import com.sun.faces.util.FacesLogger;
 
import javax.faces.application.Application;
import java.text.MessageFormat;
import java.util.logging.Level;
import java.util.logging.Logger;
 
/**
 * @author jacek@kunicki.org
 */
public class FixedEnumConverterApplicationFactoryImpl
    extends ApplicationFactoryImpl {
 
    private Application application;
    private static final Logger logger =
        FacesLogger.APPLICATION.getLogger();
 
    @Override
    public Application getApplication() {
        if (application == null) {
            application =
                new FixedEnumConverterApplicationImpl();
            if (logger.isLoggable(Level.FINE)) {
                logger.fine(MessageFormat.format(
                    "Created Application instance ''{0}''",
                    application)
                );
            }
        }
 
        return application;
    }
}
The final step is to register a custom application factory in faces-config.xml:
<factory>
<application-factory>
org.kunicki.jsf.FixedEnumConverterApplicationFactoryImpl
</application-factory>
</factory>
So, this one worked for me. Of course you can simply avoid overriding methods in your enums or perhaps use MyFaces, which may not contain this bug. If you think this is a nice or bad solution, please leave a comment.