Realize a Product Line with Tapestry 5

In this article about how to track the logins and logouts of users the AppModule has been used to add some configuration to a Tapestry application. The current article is about adding a product line to a Tapestry application in terms of localized message strings dependent on the language as well as the chosen product. The corresponding project may be found on github.

The implementation uses again a page render request to retrieve the information which product has been chosen. The selection is done by adding a path component with the product name to the URL of the respective site (e.g., /productOne):

public PageRenderRequestFilter buildProductFilter(
    final ProductIndicator productIndicator) {
  return new PageRenderRequestFilter() {
    public void handle(PageRenderRequestParameters parameters,
        PageRenderRequestHandler handler) throws IOException {
      EventContext activationContext = parameters.getActivationContext();
      String productName = activationContext.get(String.class, 0);
      if(!(productName == null || productName.isEmpty())) {
        productIndicator.setProduct(Product.valueOf(productName));
      }
      handler.handle(parameters);
    }
  };
}

The path component is retrieved from the activation context (see activationContext.get(String.class, 0)). The product indicator then saves the selected product line for the complete request:

@Scope(ScopeConstants.PERTHREAD)
public class ProductIndicatorImpl implements ProductIndicator {
  private Product product;
  public Product getProduct() {
    return product;
  }
  public void setProduct(Product product) {
    this.product = product;
  }
}

Moreover, two more classes are necessary: a component resource selector analyzer and the custom component resource selector (CRS) itself. The former determines, which CRS should be used and sets a so called axis. An axis is a further (custom) dimension in addition to the locales (i.e. languages):

public ComponentResourceSelector buildSelectorForRequest() {
  Locale locale = threadLocale.getLocale();
  Product product = productIndicator.getProduct();
  return new ComponentResourceSelector(locale)
    .withAxis(Product.class, product);
}

The Product-class itself is just an enum with all products in the product line. The latter (i.e. the CRS) then takes the product axis and adds the corresponding message files to the message catalogs:

public List<Resource> locateMessageCatalog(Resource baseResource, 
    ComponentResourceSelector selector) {
  Product product = selector.getAxis(Product.class);
  if (product != null) {
    String fileName = baseResource.getFile();
    fileName = fileName.replace(
      ".properties", "-" + product + ".properties");
    Resource productResource = baseResource.forFile(fileName);
    if (productResource.exists()) {
      List<Resource> messageCatalogs = new LinkedList<Resource>();
      messageCatalogs.addAll(
        delegate.locateMessageCatalog(productResource, selector));
      messageCatalogs.addAll(
        delegate.locateMessageCatalog(baseResource, selector));
      return messageCatalogs;
    }
  }
  return delegate.locateMessageCatalog(baseResource, selector);
}

The nice thing here is, that a product may set just some differing messages. Everything, that stays the same for several products can be omitted. Just like for languages, the fallback mechanism will check first, whether there is an implementation in the current product and then whether there is a fallback in the good old generic messages files. This can even be extended to support a chain of fallbacks to more generic products until the standard message files are queried. The multi-language support is not affected by this feature.

Since this time I have a nice small github repository, I won’t get into details here, how to register the CSR and its analyzer to Tapestry. Just have a look at the AppModule class in the github project.

Furthermore, the chosen product may be set in the page and since components do not have a activation context, they cannot (easily) access this information. With this page render request filter used here components gain access to this information implicitly. This feature can be used for much more elaborate things, which I will tackle in a future post. Ex(c)icing.

Leave a Reply

Your email address will not be published. Required fields are marked *

Captcha * Time limit is exhausted. Please reload CAPTCHA.