Tapestry Magic #3: Plugin Blocks

Blocks in Tapestry are tools which can be used to overcome the limitations of a static structure. One of the best usage of Blocks is the BeanBlockSource. I have used it in a similar way to create plug-n-play plug-in. I will try to demonstrate it with an example.

We recently did that in a case where in we had a department with a web-application that was based on the concept of an Instruction. Other departments login to the application and create instructions(with maker-checker concept). This department has to process the instructions. The problem is that the department itself consists of sub-departments and each instruction had to be delegated to them. So, the department is responsible for the authorization, authentication, logging, instruction creation(maker-checker concept). etc and the sub-department have to just get their additional details related to the instruction and process them and the return the control back to the application.

To solve this problem we have to do two things.

  1. Allow each sub-department to add their input components/sub-form to the instruction form.
  2. Allow each department to contribute a service to process the instruction.

Contributing a service is something we can easily do. Just add it to your Module. Changing an instruction form based on sub-department could be done by

...
<form t:type='form'>
   <t:commonFormContent/>
   <t:delegate to='departmentBlock'/>
</form>

<t:block t:id='debit_card_department'>
   <t:debitCardDepartmentFormDetails/>
</t:block>

<t:block t:id='credit_card_department'>
   <t:creditCardDepartmentFormDetails/>
</t:block>

....

and then in the class file


public class CommonForm {
   public Block getDepartmentBlock(){
      return resources.getComponentResources().getBlock(department_name);
   }
}

But this has a limitation, I have to change the template of the main web-application each time I add a new sub-department or if a sub-department adds an new instruction type. In order to make each sub-department jar as a plug-in-play plugin we came up with the following solution.

Create an InstructionSource service to which each sub-department can contribute a block and a processing service.

The processing service is simple


public interface InstructionProcessor {
   String process(Instruction instruction);
}

The InstructionSource service is the one to which contributions have to be made

public interface InstructionSource {
   public Block getBlock(String instructionType);
   public InstructionProcessor getProcessor(String instructionType);
}

The contribution will be


public class InstructionContribution {
   
   private String pageName;
   private String blockName;
   private String serviceId;

   public InstructionContribution(String pageName, String blockName, String serviceId){
      this.pageName = pageName;
      this.blockName = blockName;
      this.serviceId = serviceId;
   }
   
   public String getPageName(){
      return pageName;
   }
   
   public String getBlockName(){
      return blockName;
   }
   
   public String getServiceId(){
      return serviceId;
   }
   
}

The contribution has to be the serviceId, as each service will be implementing the common interface InstructionProcessor, the page containing the instruction block and the block’s id. Now finally the InstructionSource service is implemented.


import org.apache.tapestry5.Block;
import org.apache.tapestry5.services.ComponentSource
import org.apache.tapestry5.ioc.ObjectLocator;

public class InstructionSourceImpl implements InstructionSource {
   private Map<String, InstructionContribution> contributions;
   private ComponentSource componentSource;
   private ObjectLocator locator;

   public InstructionSourceImpl(final ObjectLocator locator,
         final ComponentSource componentSource,
         Map<String, InstructionContribution> contribution) {
      this.locator = locator;
      this.contributions = contribution;
      this.componentSource = componentSource;
   }

   public Block getBlock(String instructionType) {
      final InstructionContribution contribution = contributions
            .get(instructionType);
      if (contribution == null) {
         throw new RuntimeException(
               "Instruction Contribution Not Found for instruction type: "
                     + instructionType);
      }

      return componentSource.getPage(contribution.getPageName())
            .getBlock(contribution.getBlockName());
   }

   public InstructionProcessor getProcessor(String instructionType) {
      final InstructionContribution contribution = contributions
            .get(instructionType);
      if (contribution == null) {
         throw new RuntimeException(
               "Instruction Contribution Not Found for instruction type: "
                     + instructionType);
      }

      return locator.getService(contribution.getServiceId(),
            InstructionProcessor.class);
   }

}

The getBlock method gets the contributed block from the page and getProcessor gets the contributed processor.

So all the sub-departments have to do is create a page containing their contributed blocks

<t:container xmlns:t='http://tapestry.apache.org/schema/tapestry_5_1_0.xsd'>
   <t:block t:id='myFirstBlock'></t:block>
   ...
</t:container>

with a page


public class MyContributedPage {
}

and then make contributions to the InstructionSource service.

public class DebitCardModule {
   @Contribute(InstructionSource.class)
   public void contributeInstructionSource(
         MappedConfiguration<String, InstructionContribution> contribution) {
      contribution.add("myFirstBlock", new InstructionContribution(
            "debitCardModule/instructionblocks", "myFirstBlock", null));
   }
}
About these ads

Tagged: ,

13 thoughts on “Tapestry Magic #3: Plugin Blocks

  1. Robert Zeigler April 20, 2011 at 11:19 PM Reply

    Nice post, just one comment. Right now, your InstructionSource service is relying on an internal tapestry service, RequestPageCache, to get the page. Internal services are not subject to backwards compatibility constraints, so a better way to do this would be to use the org.apache.tapestry5.services.ComponentSource service. This service is public, and you can easily replace:

    return pageCache.get(contribution.getPageName()).getRootElement()
    .getBlock(contribution.getBlockName());

    with:

    return componentSource.getPage(contribution.getPageName()).getBlock(contribution.getBlockName());

    ComponentSource handles fetching the root object of the page for you, so your code gets a little cleaner, as well.

    Cheers.

    • tawus April 20, 2011 at 11:26 PM Reply

      Thanks, at times you peek so much into the source code and you fail to checkout the javadocs :)

  2. Rendy May 4, 2011 at 3:30 PM Reply

    For beginner tapestry user, I’ve bit confused with what the last shape of CommonForm class and tml file look a like after that changes.

    Could you just provide a link for complete source to download or the form of that two file I need above ?

    Thanks.

    • tawus May 6, 2011 at 11:14 PM Reply

      I am thinking of hosting source code of all the examples on github, hopefully next weekend. But for a beginner I will suggest trying jumpstart first

  3. Rendy May 9, 2011 at 12:25 PM Reply

    Thanks for the link, but I’ll still looking forward for the code @Github.

  4. Rendy July 17, 2011 at 4:50 PM Reply

    Hi Taha,

    Please upload your source of this article to github…

  5. tux4ever August 30, 2011 at 1:01 PM Reply

    We use in our project the same mechanism and we ran into event handling problems. If a component inside the contributed blocks triggers an event the ContributedPages gets the event and not the injected Page. Do you have a solution for this topic?

    Another problem we are struggling with is the parameter mechanism. It would be very helpful if the target page could provide some arguements for the contributed block/component (e.g. Id of an entity to know which person is edited,…). Did you every try to realize this mechanism?

    • tawus September 5, 2011 at 3:49 PM Reply

      There is no easy solution for the first part. The only way to solve it (at least that I can think of) is to add mixins during transformations like in this article

      For the second part, you can pass information using Environment service.

  6. Muhammad Gelbana August 29, 2012 at 6:43 PM Reply

    I REALLY like this post :)
    I’m currently trying to inject my block but tapestry keeps complaining about not being able to find my page (the page I have in my module). So I have some questions regarding that matter:

    1. On what basis did you name your page (yourPage’sModuleName/youPageName)
    2. Can the contributed block link to pages in it’s own module or the web-app’s module ? And how can that be done ?!
    3. About “InstructionSourceImpl” service, is it located in each contributing module or in the main app’s module ? Because I think may be this service’s “ComponentSource” can only inject the needed page if it’s in the same module.

  7. Lance December 13, 2012 at 5:31 PM Reply

    Great post! I think that the Page contributions should use the @WhitelistAccessOnly so that the pages cannot be accessed by typing in the URL.

    eg:
    @WhitelistAccessOnly
    public class MyContributedPage {
    }

  8. avin.ws January 4, 2013 at 8:07 PM Reply

    I blog too and I’m crafting a thing alike to this particular posting, “Tapestry Magic #3:
    Plugin Blocks Java Magic”. Do you care in case I reallyincorporate a number of of your points?
    Regards -Osvaldo

    • tawus January 4, 2013 at 8:49 PM Reply

      Please feel free to point to or use any of the content. Thanks for asking. BTW I have seen my posts on chinese websites and I still can’t figure out how it reached there :)

  9. Juan E. Maya January 8, 2014 at 6:20 PM Reply

    Very useful post !!! Thank you, also i can report that the problem with the block events has been resolved already, at least they work for us on Tapestry 5.3.7

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 91 other followers

%d bloggers like this: