jugLviv

Meta


Share on:


Hacking the enum conversion in JSF 1.2_x

JUG LvivJUG Lviv
Сьогодні зіткнувся з тим що 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.

JUG Lviv
Author