Tapestry Magic #6: Environment

Environment is yest another Tapestry’s Magic Trick which changes the way you approach a problem once you get to know it. Environment service keeps a stack for each interface on which you can push an implementation. This service is available per-thread, so a new instance is available for each request cycle. During the request cycle you can change the implementation at any point by pushing your own implementation over the one available and later when you are done using it, popping out yours and restoring the previous one from the stack. There are two main uses of Environment service, passing information to nested components and providing a per-thread service which can be overridden at any point in the request cycle. You can read more about it here.

In this post, I will implement Notifications. The idea is this. I can add notifications while processing a request and these notifications will be shown to the user using Prototype based Growl.

First we need a service which can be used to add and retrieve notifications


public interface Notifications {
   void inform(String message);
   void warn(String message);
   void error(String error);
   boolean getHasMessages();
   List<String> getInformations();
   List<String> getWarnings();
   List<String> getErrors();
   void clear();
}

Its implementation is very simple.

public class NotificationsImpl implements Notifications {
   private List<String> informations = new ArrayList<String>();
   private List<String> warnings = new ArrayList<String>();
   private List<String> errors =new ArrayList<String>();

   public void inform(String message) {
      informations.add(message);      
   }

   public void warn(String message) {
      warnings.add(message);      
   }
   
   public void error(String message) {
      errors.add(message);      
   }
   
   public boolean getHasMessages(){
      return informations.size() != 0 || warnings.size() != 0 ||
         errors.size() != 0;
   }
   
   public List<String> getInformations(){
      return informations;
   }
   
   public List<String> getWarnings(){
      return warnings;
   }
   
   public List<String> getErrors(){
      return errors;
   }
   
   public void clear(){
      informations.clear();
      warnings.clear();
      errors.clear();
   }

}

Now we need to put this service into the Environment that can later be accessed using @Environmental annotation. Now the question is where to put the service into the Environment. If we want the environment to be available only during rendering of a page, we can implement PageRenderRequestFilter and put it in the PageRenderRequest pipeline by contributing to PageRenderRequestHandler. If we want the environment to be available only during component events, we can implement ComponentEventRequestFilter and put it in the ComponentEventRequest pipeline by contributing to ComponentEventRequestHandler. But in our case we want it for both Page rendering and during Component Event handling, so we implement ComponentRequestFilter and then put it in the ComponentRequest pipeline by adding it to ComponentRequestHandler.

public class NotificationEnvironmentSetupHandler implements
      ComponentRequestFilter {

   private Environment environment;

   public NotificationEnvironmentSetupHandler(Environment environment) {
      this.environment = environment;
   }

   public void handleComponentEvent(ComponentEventRequestParameters params,
         ComponentRequestHandler handler) throws IOException {
      environment.push(Notifications.class, new NotificationsImpl());
      handler.handleComponentEvent(params);
   }

   public void handlePageRender(PageRenderRequestParameters params,
         ComponentRequestHandler handler) throws IOException {
      environment.push(Notifications.class, new NotificationsImpl());
      handler.handlePageRender(params);
   }

}

ComponentRequestFilter has two methods for handling Page Render requests and Component Event requests. We put our service implementation on the Environment stack in both the methods and allow the pipeline to proceed. Next we put our filter in the pipeline


   public void contributeComponentRequestHandler(Environment environment,
         OrderedConfiguration<ComponentRequestFilter> contributions) {
      contributions.add("notifications",
            new NotificationEnvironmentSetupHandler(environment));
   }

Once the environment is setup, we can add notifications as

public class TestPage {
   @Environmental
   private Notifications notifications;

   public void onActivate(){
      notifications.inform("You are now logged in");
   }
}

We need to show these messages using Growl. As we want it across all pages, we use another pipeline MarkupRenderer. We implement MarkupRendererFilter and contribute to the MarkupRendererHandler.

public class NotificationsMarkupRendererFilter implements MarkupRendererFilter {
   private Environment environment;
   private Asset notificationsScript;

   public NotificationsMarkupRendererFilter(Asset notificationsScript,
         Environment environment) {
      this.environment = environment;
      this.notificationsScript = notificationsScript;
   }

   public void renderMarkup(MarkupWriter writer, MarkupRenderer renderer) {
      if (environment.peek(Notifications.class) != null
            && environment.peek(JavaScriptSupport.class) != null) {
         Notifications notifications = environment.pop(Notifications.class);
         JavaScriptSupport javaScriptSupport = environment
               .peek(JavaScriptSupport.class);
         if (notifications.getHasMessages()) {
            JSONObject spec = new JSONObject();
            spec.put("informations", new JSONArray(notifications
                  .getInformations().toArray()));
            spec.put("warnings", new JSONArray(notifications.getWarnings()
                  .toArray()));
            spec.put("errors", new JSONArray(notifications.getErrors()
                  .toArray()));
            String url = notificationsScript.toClientURL();
            if(!url.endsWith("/")){
               url = url.substring(0, url.lastIndexOf("/")+1);
            }
            spec.put("url", url);
            javaScriptSupport.importJavaScriptLibrary(notificationsScript);
            javaScriptSupport.addScript(InitializationPriority.LATE,
                  "Notifications.display(%s);", spec);
            notifications.clear();
         }
      }
      renderer.renderMarkup(writer);
   }
}

What we are doing here is using JavaScriptSupport to import our script and passing it the messages. Finally we make the contribution

   public void contributeMarkupRenderer(
         @Inject @Path("assets/notifications.js") Asset notificationsScript,
         Environment environment,
         OrderedConfiguration<MarkupRendererFilter> contributions) {
      contributions.add("notifications", new NotificationsMarkupRendererFilter(
            notificationsScript, 
            environment), "after:JavascriptSupport");
   }

      public static void contributeClasspathAssetAliasManager(
         MappedConfiguration<String, String> configuration) {
      configuration.add("tawusassets", "assets");
   }

That is it!!

You can make your notification service directly accessible through @Inject by using EnvironmentalShadowBuilder as shown below

   public Notifications buildNotifications(EnvironmentalShadownBuilder shadowBuilder){
      return shodowBuilder.build(Notifications.class);
   }

A note on the Growl script. I had to make some small changes to the script. The images in the script are hard-coded, in order to make them relative to the javascript, I prepend the url part of the asset location to each image location.

var Growl =  {
    initialize: function(prefix){
        this.oBezel = new Gr0wl.Bezel(prefix + "bezel.png");
        this.Bezel =  this.oBezel.show.bind(this.oBezel);
        this.oSmoke = new Gr0wl.Smoke(prefix + "smoke.png");
        this.Smoke = this.oSmoke.show.bind(this.oSmoke);
    }
};

Notifications = {
   display:function(spec){  
      Growl.initialize(spec.url);
      for(var i = 0; i < spec.errors.length; ++i){
         Growl.Smoke({
            title: "Error",
            text: spec.errors[i],
            image: spec.url + "error.png",
            duration: 10.0
        });
      }
      for(var i = 0; i < spec.warnings.length; ++i){
         Growl.Smoke({
            title: "Warning",
            text: spec.warnings[i],
            image: spec.url + "warn.png",
            duration: 2.0
        });
      }
         for(var i = 0; i < spec.informations.length; ++i){
            Growl.Smoke({
               title: "Information",
               text: spec.informations[i],
               image: spec.url + "inform.png",
               duration: 5.0
           });
         }
      }
};
About these ads

Tagged: , ,

6 thoughts on “Tapestry Magic #6: Environment

  1. Dimitris Zenios April 27, 2011 at 9:06 PM Reply

    Very nice post.Well done.There is a small typo though

    spec.put(“errors”, new JSONArray(notifications.getWarnings()
    .toArray()));

    should be

    spec.put(“errors”, new JSONArray(notifications.getErrors()
    .toArray()));

    • tawus April 27, 2011 at 10:02 PM Reply

      Thanks for pointing it out.. Updated the post

  2. Raúl April 27, 2011 at 10:15 PM Reply

    Good post, but I have one question: imagine you receive a component event request for deleting an item of a list. You could want to send a notification to the user, something like “Item deleted”. The problem is that, unless it is an Ajax request, all notifications stored in that service in the component event request will be lost after the redirect to the page render request, so the user will never see those notifications.

    Regards,
    Raul.

    • tawus April 28, 2011 at 12:57 AM Reply

      I am using these notifications in one of my projects and I only wanted the notifications for simple cases. Besides it was easy for explaining Environment.
      For dealing with redirect-after-post case, we can use ApplicationStateManager. You have just given me an idea for a new post :)

      Thanks Raul

      • Raul April 28, 2011 at 7:45 AM

        :) Well, I’ll wait for that post then ;) I didn’t want to involve the session into this, but I suppose a flash persistence is the only possible approach to this…

      • tawus April 28, 2011 at 7:51 PM

        Already done!!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 90 other followers

%d bloggers like this: