The JSF custom scope allows us to implement our own scopes
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.
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 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) {
throw new PropertyNotFoundException("NULL_PARAMETERS");
}
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) {
throw new PropertyNotFoundException("NULL_PARAMETERS");
}
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="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="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 ...");
}
}
}
The complete application is available 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>
beans.CustomScopeNavigationHandler
</navigation-handler>
The complete application is available here.
For testing on localhost,
use two browsers!
Niciun comentariu :
Trimiteți un comentariu