The custom scope
When none of the previous scopes meet your application needs, you have to pay attention to the JSF 2 custom scope. Most likely, you will never want to write a custom scope, but if it is necessary, then, in this section, you can see how to accomplish this task.
Note
The custom scope annotation is @CustomScoped
and is defined in the javax.faces.bean
package. It is not available in CDI!
In order to implement a custom scope, let's suppose that you want to control the life cycle of several beans that live in the application scope. Normally they live as long as the application lives, but you want to be able to add/remove them from the application scope at certain moments of the application flow. Of course, there are many approaches to do that, but remember that we look for a reason to implement a custom scope; therefore, we will try to write a custom scope nested in the application scope that will allow us to add/remove a batch of beans. Creating and destroying the scope itself will be reflected in creating and destroying the beans, which means that you don't need to refer to each bean.
Actually, since this is just a demo, we will use only two beans: one will stay in the classical application scope (it can be useful for comparison of the application and custom scope lifespan), while the other one will be added/destroyed through the custom scope. The application purpose is not relevant; you should focus on the technique used to write a custom scope and paper over the assumptions and gaps. Think more on the lines that you can use this knowledge when you really need to implement a custom scope.
Writing the custom scope class
The custom scope is represented by a class that extends the ConcurrentHashMap<String, Object>
class. We need to allow concurrent access to an usual map because the exposed data may be accessed concurrently from multiple browsers. The code of the CustomScope
class is as follows:
public class CustomScope extends ConcurrentHashMap<String, Object> { public static final String SCOPE = "CUSTOM_SCOPE"; public CustomScope(){ super(); } public void scopeCreated(final FacesContext ctx) { ScopeContext context = new ScopeContext(SCOPE, this); ctx.getApplication().publishEvent(ctx, PostConstructCustomScopeEvent.class, context); } public void scopeDestroyed(final FacesContext ctx) { ScopeContext context = new ScopeContext(SCOPE,this); ctx.getApplication().publishEvent(ctx, PreDestroyCustomScopeEvent.class, context); } }
When our scope is created/destroyed, other components will be informed through events. In the scopeCreated
method, you register PostConstructCustomScopeEvent
, while in the scopeDestroyed
method, you register PreDestroyCustomScopeEvent
.
Now we have a custom scope, it is time to see how to declare a bean in this scope. Well, this is not hard and can be done with the @CustomScoped
annotations and an EL expression, as follows:
import javax.faces.bean.CustomScoped; import javax.faces.bean.ManagedBean; @ManagedBean @CustomScoped("#{CUSTOM_SCOPE}") public class SponsoredLinksBean { ... }
Resolving a custom scope EL expression
At this point, JSF will iterate over the chain of existing resolvers in order to resolve the custom scope EL expression. Obviously, this attempt will end with an error, since no existing resolver will be able to satisfy this EL expression. So, you need to write a custom resolver as you saw in Chapter 1, Dynamic Access to JSF Application Data through Expression Language (EL 3.0). Based on that, you should obtain something as shown in the following code:
public class CustomScopeResolver extends ELResolver { private static final Logger logger = Logger.getLogger(CustomScopeResolver.class.getName()); @Override public Object getValue(ELContext context, Object base, Object property) { logger.log(Level.INFO, "Get Value property : {0}", property); if (property == null) { String message = MessageUtils.getExceptionMessageString(MessageUtils.NULL_PARAMETERS_ERROR_MESSAGE_ID, "property"); throw new PropertyNotFoundException(message); } FacesContext facesContext = (FacesContext) context.getContext(FacesContext.class); if (base == null) { Map<String, Object> applicationMap = facesContext.getExternalContext().getApplicationMap(); CustomScope scope = (CustomScope) applicationMap.get(CustomScope.SCOPE); if (CustomScope.SCOPE.equals(property)) { logger.log(Level.INFO, "Found request | base={0} property={1}", new Object[]{base, property}); context.setPropertyResolved(true); return scope; } else { logger.log(Level.INFO, "Search request | base={0} property={1}", new Object[]{base, property}); if (scope != null) { Object value = scope.get(property.toString()); if (value != null) { logger.log(Level.INFO, "Found request | base={0} property={1}", new Object[]{base, property}); context.setPropertyResolved(true); }else { logger.log(Level.INFO, "Not found request | base={0} property={1}", new Object[]{base, property}); context.setPropertyResolved(false); } return value; } else { return null; } } } if (base instanceof CustomScope) { CustomScope baseCustomScope = (CustomScope) base; Object value = baseCustomScope.get(property.toString()); logger.log(Level.INFO, "Search request | base={0} property={1}", new Object[]{base, property}); if (value != null) { logger.log(Level.INFO, "Found request | base={0} property={1}", new Object[]{base, property}); context.setPropertyResolved(true); } else { logger.log(Level.INFO, "Not found request | base={0} property={1}", new Object[]{base, property}); context.setPropertyResolved(false); } return value; } return null; } @Override public Class<?> getType(ELContext context, Object base, Object property) { return Object.class; } @Override public void setValue(ELContext context, Object base, Object property, Object value) { if (base != null) { return; } context.setPropertyResolved(false); if (property == null) { String message = MessageUtils.getExceptionMessageString(MessageUtils.NULL_PARAMETERS_ERROR_MESSAGE_ID, "property"); throw new PropertyNotFoundException(message); } if (CustomScope.SCOPE.equals(property)) { throw new PropertyNotWritableException((String) property); } } @Override public boolean isReadOnly(ELContext context, Object base, Object property) { return true; } @Override public Iterator<FeatureDescriptor> getFeatureDescriptors(ELContext context, Object base) { return null; } @Override public Class<?> getCommonPropertyType(ELContext context, Object base) { if (base != null) { return null; } return String.class; } }
Do not forget to put the following resolver into the chain by adding it in the faces-config.xml
file:
<el-resolver>book.beans.CustomScopeResolver</el-resolver>
Done! So far, you have created a custom scope, you put a bean into this scope, and learned that the brand new resolver provides access to this bean.
The custom scope must be stored somewhere, so nested in the application scope can be a choice (of course, other scopes can also be a choice, depending on your needs). When the scope is created, it has to be placed in the application map, and when it is destroyed, it has to be removed from the application map. The question is when to create it and when to destroy it? And the answer is, it depends. Most likely, this is a decision strongly tied to the application flow.
Controlling the custom scope lifespan with action listeners
Using action listeners can be a good practice even if it involves control from view declaration. Let's suppose that the button labeled START will add the custom scope in the application map, as shown in the following code:
<h:commandButton value="START"> <f:actionListener type="book.beans.CreateCustomScope" /> </h:commandButton>
The following CreateCustomScope
class is a straightforward action listener as it implements the ActionListener
interface:
public class CreateCustomScope implements ActionListener { private static final Logger logger = Logger.getLogger(CreateCustomScope.class.getName()); @Override public void processAction(ActionEvent event) throws AbortProcessingException { logger.log(Level.INFO, "Creating custom scope ..."); FacesContext context = FacesContext.getCurrentInstance(); Map<String, Object> applicationMap = context.getExternalContext().getApplicationMap(); CustomScope customScope = (CustomScope) applicationMap.get(CustomScope.SCOPE); if (customScope == null) { customScope = new CustomScope(); applicationMap.put(CustomScope.SCOPE, customScope); customScope.scopeCreated(context); } else { logger.log(Level.INFO, "Custom scope exists ..."); } } }
Following the same approach, the button labeled STOP will remove the custom scope from the application map as follows:
<h:commandButton value="STOP"> <f:actionListener type="book.beans.DestroyCustomScope" /> </h:commandButton>
The following DestroyCustomScope
class is the action listener as it implements the ActionListener
interface:
public class DestroyCustomScope implements ActionListener { private static final Logger logger = Logger.getLogger(DestroyCustomScope.class.getName()); @Override public void processAction(ActionEvent event) throws AbortProcessingException { logger.log(Level.INFO, "Destroying custom scope ..."); FacesContext context = FacesContext.getCurrentInstance(); Map<String, Object> applicationMap = context.getExternalContext().getApplicationMap(); CustomScope customScope = (CustomScope) applicationMap.get(CustomScope.SCOPE); if (customScope != null) { customScope.scopeDestroyed(context); applicationMap.remove(CustomScope.SCOPE); } else { logger.log(Level.INFO, "Custom scope does not exists ..."); } } }
This example is wrapped into the application named ch3_8
that is available in the code bundle of this chapter. Just a run and a quick look over the code will clarify that the spaghetti-code is missing here.
Controlling the custom scope lifespan with the navigation handler
Another approach is to control the custom scope lifespan based on the page's navigation. This solution is more flexible and is hidden from the user. You can write a custom navigation handler by extending NavigationHandler
. The next implementation puts the custom scope in the application map when the navigation reaches the page named sponsored.xhtml
, and will remove it from the application map in any other navigation case. The code of the CustomScopeNavigationHandler
class is as follows:
public class CustomScopeNavigationHandler extends NavigationHandler { private static final Logger logger = Logger.getLogger(CustomScopeNavigationHandler.class.getName()); private final NavigationHandler navigationHandler; public CustomScopeNavigationHandler(NavigationHandler navigationHandler) { this.navigationHandler = navigationHandler; } @Override public void handleNavigation(FacesContext context, String fromAction, String outcome) { if (outcome != null) { if (outcome.equals("sponsored")) { logger.log(Level.INFO, "Creating custom scope ..."); Map<String, Object> applicationMap = context.getExternalContext().getApplicationMap(); CustomScope customScope = (CustomScope) applicationMap.get(CustomScope.SCOPE); if (customScope == null) { customScope = new CustomScope(); applicationMap.put(CustomScope.SCOPE, customScope); customScope.scopeCreated(context); } else { logger.log(Level.INFO, "Custom scope exists ..."); } } else { logger.log(Level.INFO, "Destroying custom scope ..."); Map<String, Object> applicationMap = context.getExternalContext().getApplicationMap(); CustomScope customScope = (CustomScope) applicationMap.get(CustomScope.SCOPE); if (customScope != null) { customScope.scopeDestroyed(context); applicationMap.remove(CustomScope.SCOPE); } else { logger.log(Level.INFO, "Custom scope does not exist"); } } } navigationHandler.handleNavigation(context, fromAction, outcome); } }
Do not forget to register the following navigation handler in the faces-config.xml
file:
<navigation-handler> book.beans.CustomScopeNavigationHandler </navigation-handler>
This example is wrapped into the application named ch3_9
that is available in the code bundle of this chapter. A quick look over the code will clarify that the spaghetti-code is missing here.
As I said earlier, JSF 2.2 comes with a wrapper class for NavigationHandler
. This is a simple implementation that can be easily extended by developers. An instance of the class being wrapped is returned in the getWrapped
method. For example, you can rewrite the CustomScopeNavigationHandler
class, as shown in the following code:
public class CustomScopeNavigationHandler extends NavigationHandlerWrapper { private static final Logger logger = Logger.getLogger(CustomScopeNavigationHandler.class.getName()); private final NavigationHandler navigationHandler; public CustomScopeNavigationHandler(NavigationHandler navigationHandler){ this.navigationHandler = navigationHandler; } @Override public void handleNavigation(FacesContext context, String fromAction, String outcome) { if (outcome != null) { if (outcome.equals("sponsored")) { logger.log(Level.INFO, "Creating custom scope ..."); Map<String, Object> applicationMap = context.getExternalContext().getApplicationMap(); CustomScope customScope = (CustomScope) applicationMap.get(CustomScope.SCOPE); if (customScope == null) { customScope = new CustomScope(); applicationMap.put(CustomScope.SCOPE, customScope); customScope.scopeCreated(context); } else { logger.log(Level.INFO, "Custom scope exists ..."); } } else { logger.log(Level.INFO, "Destroying custom scope ..."); Map<String, Object> applicationMap = context.getExternalContext().getApplicationMap(); CustomScope customScope = (CustomScope) applicationMap.get(CustomScope.SCOPE); if (customScope != null) { customScope.scopeDestroyed(context); applicationMap.remove(CustomScope.SCOPE); } else { logger.log(Level.INFO, "Custom scope does not exist"); } } } getWrapped().handleNavigation(context, fromAction, outcome); } @Override public NavigationHandler getWrapped() { return navigationHandler; } }
This example is wrapped into the application named ch3_10
that is available in the code bundle of this chapter.