Mastering JavaServer Faces 2.2
上QQ阅读APP看书,第一时间看更新

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.