Thursday, April 19, 2007

Creating a Portlet Preferences JSF VariableResolver

One of the really cool things about JSF is that just about every piece of it is extensible. One thing I've been playing with recently is the VariableResolver and PropertyResolver parts of the JSF API. I want to create a VariableResolver that is specific to portlet development, so that, for example, when you want to create an EDIT page where a user can specify a preferred nickname, it would look something like this:

<h:inputText value="#{portletPreferences.nickname}">

And that's it, because custom Variable and Property resolvers take care of the getting and setting for us, no backing beans required.

Turns out this is fairly easy to do. I started with Cagatay Civici's excellent blog/article on creating a custom VariableResolver and PropertyResolver. The first thing to do was to create the VariableResolver. The trick here is that VariableResolvers follow a "Chain of Responsibility" design pattern, and the parent resolver is passed in via the constructor of the child resolver. This is an extremely important point. I rushed ahead on the PortletPreferences part of the variable resolving and forgot to implement the chained part, and you get some interesting results when you do this. Suddenly, your managed beans just silently are unresolvable. No error messages, nothing in the logs, and in fact, it appears at first that they are working, but they do not. So just a heads up, that if you create your own JSF resolvers, do the chaining part first and look out for disappearing managed beans.

So here's how my initial PortletVariableResolver looked:


public class PortletVariableResolver extends VariableResolver {

private static final org.apache.log4j.Logger log = org.apache.log4j.Logger
.getLogger(PortletVariableResolver.class);

public static final String PORTLET_PREFERENCES = "portletPreferences";
private VariableResolver originalResolver;

public PortletVariableResolver(VariableResolver originalResolver) {
this.originalResolver = originalResolver;
}
@Override
public Object resolveVariable(FacesContext facesContext,
String variableName)
throws EvaluationException {

try {
if (PORTLET_PREFERENCES.equals(variableName)) {
PortletRequest portletRequest =
(PortletRequest)facesContext
.getExternalContext().getRequest();
return portletRequest.getPreferences().getMap();
}
} catch (Exception e) {
throw new EvaluationException(
"Failed to resolve variable [" +
variableName + "]", e);
}
return originalResolver.resolveVariable(facesContext,
variableName);
}

}


So here we're resolving the "portletPreferences" variable to the Map of the user's portlet preferences. This is nice, but it only gets you read access to the portlet preferences. For write access as well, we'll need a custom PropertyResolver. First, let's change our variable resolver so that it returns the PortletPreferences object, instead of the map:


if (PORTLET_PREFERENCES.equals(variableName)) {
PortletRequest portletRequest =
(PortletRequest)facesContext
.getExternalContext().getRequest();
return portletRequest.getPreferences();
}


Now for the PropertyResolver.


public class PortletPropertyResolver extends PropertyResolver {

private static final org.apache.log4j.Logger log = org.apache.log4j.Logger
.getLogger(PortletPropertyResolver.class);

private PropertyResolver originalPropertyResolver;

public PortletPropertyResolver(PropertyResolver propertyResolver) {
this.originalPropertyResolver = propertyResolver;
}

@Override
public Class getType(Object obj, int index) throws EvaluationException,
PropertyNotFoundException {
if (obj instanceof PortletPreferences) {
throw new PropertyNotFoundException("Cannot reference PortletPreferences by index notation");
} else {
return this.originalPropertyResolver.getType(obj, index);
}
}

@Override
public Class getType(Object obj, Object obj1) throws EvaluationException,
PropertyNotFoundException {
if (obj instanceof PortletPreferences) {
return String.class;
} else {
return this.originalPropertyResolver.getType(obj, obj1);
}
}

@Override
public Object getValue(Object obj, int index) throws EvaluationException,
PropertyNotFoundException {
if (obj instanceof PortletPreferences) {
throw new PropertyNotFoundException("Cannot reference PortletPreferences by index notation");
} else {
return this.originalPropertyResolver.getValue(obj, index);
}
}

@Override
public Object getValue(Object obj, Object obj1)
throws EvaluationException, PropertyNotFoundException {
if (obj instanceof PortletPreferences) {
// TODO: support access to String[] preference values
PortletPreferences prefs = (PortletPreferences) obj;
String prefName = (String) obj1;
return prefs.getValue(prefName, null);
} else {
return this.originalPropertyResolver.getValue(obj, obj1);
}
}

@Override
public boolean isReadOnly(Object obj, int index)
throws EvaluationException, PropertyNotFoundException {
if (obj instanceof PortletPreferences) {
throw new PropertyNotFoundException("Cannot reference PortletPreferences by index notation");
} else {
return this.originalPropertyResolver.isReadOnly(obj, index);
}
}

@Override
public boolean isReadOnly(Object obj, Object obj1)
throws EvaluationException, PropertyNotFoundException {
if (obj instanceof PortletPreferences) {
PortletPreferences prefs = (PortletPreferences) obj;
String prefName = (String) obj1;
return prefs.isReadOnly(prefName);
} else {
return this.originalPropertyResolver.isReadOnly(obj, obj1);
}
}

@Override
public void setValue(Object obj, int index, Object obj1)
throws EvaluationException, PropertyNotFoundException {
if (obj instanceof PortletPreferences) {
throw new PropertyNotFoundException("Cannot reference PortletPreferences by index notation");
} else {
this.originalPropertyResolver.setValue(obj, index, obj1);
}

}

@Override
public void setValue(Object obj, Object obj1, Object value)
throws EvaluationException, PropertyNotFoundException {
if (obj instanceof PortletPreferences) {
PortletPreferences prefs = (PortletPreferences) obj;
String prefName = (String) obj1;
String prefValue = (String) value;
try {
prefs.setValue(prefName, prefValue);
prefs.store();
} catch (Exception e) {
log.error("Unable to set portlet preference [" + prefName + "] to value [" + prefValue + "]", e);
}
} else {
this.originalPropertyResolver.setValue(obj, obj1, value);
}
}

}


I've highlighted the main two methods here, getValue and setValue, and I think you'll find them fairly straightforward. The other issue is that we have to implement the array index part of the property resolving, and we just throw an exception in this case (although, as a further exercise, we might want to implement this so that we can reference multi-valued preferences).

At this point, you just need to register the VariableResolver and PropertyResolver in your faces-config.xml file, like so:


<application>
<property-resolver>
resolver.PortletPropertyResolver
</property-resolver>
<variable-resolver>
resolver.PortletVariableResolver
</variable-resolver>
</application>


That's it.