Gang,

Here at CodeDragons, we have been working the last days on getting the OSGi 
support for Wicket totally right, and we have come quite a far bit on the 
way, but not there yet. But I thought we should inform of progress and some 
code examples of what actually works.

This work has been sponsored by ScanCoin, for which we are eternally grateful.

We know that at least two other parties are very keen on how to do this, so 
let's look into the details. The source is available in the OPS4J subversion 
repository at https://scm.ops4j.org/repos/ops4j/projects/pax/wicket/ and the 
sub directories of service/ and samples/department-store. (Ignore the other 
directories)

I will update the Pax Wicket documentation on the Wiki later today and/or 
tomorrow...

- o - o - o -

The first and foremost goal was to allow for any Wicket component to be stuck 
into any bundle and allow for dynamic load and unload of such resources, 
without any significant compromises on the Wicket functionality.

The second goal was to make it easy to create re-usable components, that can 
be deployed in their own bundles, and together with the first goal, upgraded 
on-the-fly without taking down the servlet.


- o - o - o -

The important part regarding Wicket is that you can't construct the Wicket 
component until the HTML and Java code matches. That means for each html tag 
marked with a wicket:id, there must be a component at the right hierarchy 
level of the Java object tree. So it was important to recognize that we need 
a model of the content independent of the Wicket instantiation process.

Our solution centers around 2 interfaces and their implementations.

public interface Content
{
    String CONFIG_DESTINATIONID = "destinationId";
    String DESTINATIONID_UNKNOWN = "";

    String getDestinationId();
    Component createComponent();
}

public interface ContentContainer
{
    String CONFIG_CONTAINMENTID = "containmentId";
    String getContainmentID();
    List<Component> createComponents( String id );
    void dispose();
}


Essentially, the Content is the source and the ContentContainer is the sink 
for components, and then the PageContent is the holder of the component tree.

Now, by leveraging the OSGi Service layer, we defined;
 "A ContentContainer implementation must register itself as an OSGi service 
and define its ContainmentID as a configuration property. The ContainmentID 
must be unique within the OSGi framework."

 "A Content implementation must register itself as an OSGi service and define 
the DestinationID as a configuration property."

The DestionationID is an configuration property which tells the Pax Wicket 
where the component should eventually be attached to. The DestionationID 
consists of two parts, [containmentId].[wicketId].
The ContainmentID is the 'extension point' of the ContentContainer. All 
Content that has a DestinationID containing the ContainmentID of a 
ContentContainer will be attached at the WicketID of that ContentContainer.

Was that a bit dense??
Let's look at our example.

We define a Floor in a department store to have Franchisees. The layout of the 
department store is sitting in a separate model bundle and is not part of 
this explanation.

So we define a FloorPanel in the FloorView bundle;

public class FloorContentContainer extends DefaultContentContainer
{
    private final Floor m_floor;

    public FloorContentContainer( 
        Floor floor, 
        String containmentId, 
        String destinationId,
        BundleContext bundleContext )
    {
        super( containmentId, destinationId, bundleContext );
        m_floor  = floor;
    }

    protected Component createComponent( String id )
    {
        return new FloorPanel( id, this, m_floor );
    }

    protected void removeComponent( Component component )
    {
        //TODO: Auto-generated, need attention.
    }

}

And we register the floors in the Activator;

public class Activator
    implements BundleActivator
{
    private List<FloorContentContainer> m_containers;
    private List<ServiceRegistration> m_registrations;

    public Activator()
    {
        m_containers = new ArrayList<FloorContentContainer>();
        m_registrations = new ArrayList<ServiceRegistration>();
    }

    public void start( BundleContext bundleContext )
        throws Exception
    {
        String depStoreServiceName = DepartmentStore.class.getName();
        ServiceReference depStoreServiceReference =
            bundleContext.getServiceReference( depStoreServiceName );
        DepartmentStore departmentStore = (DepartmentStore)
            bundleContext.getService( depStoreServiceReference );
        List<Floor> floors = departmentStore.getFloors();
        String destinationId = "swp.floor";
        for( Floor floor : floors )
        {
            FloorContentContainer container =
                new FloorContentContainer( 
                    floor, floor.getName(), destinationId, bundleContext );
            m_containers.add( container );
            container.setDestinationId( destinationId );
            container.setContainmentId( floor.getName() );
            ServiceRegistration registration = container.register();
            m_registrations.add( registration );
        }
    }

    public void stop( BundleContext bundleContext )
        throws Exception
    {
        for( ServiceRegistration registration : m_registrations )
        {
            registration.unregister();
        }
        m_registrations.clear();
        for( ContentContainer floor : m_containers )
        {
            floor.dispose();
        }
    }
}

What is important to notice is that we create the FloorContentContainer, sets 
it ContainmentID (setting the DestinationID is all about inserting the Floor 
in the right place of the building) and calls the register() method in the 
superclass.

The DefaultContentContainer implements all the trickier bits to get this to 
work, and we are trying to fulfill the second goal of making it relatievly 
easy to create components.

Ok. So how about the Franchisees on each floor??

public class FranchiseeContent extends DefaultContent
{

    private Franchisee m_franchisee;

    public FranchiseeContent( BundleContext context, Franchisee franchisee )
    {
        super( context, franchisee.getName()  );
        m_franchisee = franchisee;
    }

    protected Component createComponent( String id )
    {
        return new FranchiseePanel( id, m_franchisee );
    }
}

And registering each Franchisee to their respective floor (by consulting the 
model).

public class Activator
    implements BundleActivator
{
    private List<ServiceRegistration> m_registrations;

    public Activator()
    {
        m_registrations = new ArrayList<ServiceRegistration>();
    }

    public void start( BundleContext bundleContext )
        throws Exception
    {
        String depStore = DepartmentStore.class.getName();
        ServiceReference depStoreService =
            bundleContext.getServiceReference( depStore );
        DepartmentStore departmentStore = (DepartmentStore)
            bundleContext.getService( depStoreService );

        m_registrations = new ArrayList<ServiceRegistration>();
        List<Floor> floors = departmentStore.getFloors();
        for( Floor floor: floors )
        {
            List<Franchisee> franchisees = floor.getFranchisees();
            for( Franchisee franchisee : franchisees )
            {
                String destinationId = floor.getName() + ".franchisee";
                FranchiseeContent content = 
                    new FranchiseeContent( bundleContext, franchisee  );
                content.setDestinationId( destinationId );
                ServiceRegistration registration = content.register();
                m_registrations.add( registration );
            }
        }
    }

    public void stop( BundleContext bundleContext )
        throws Exception
    {
        for( ServiceRegistration registeration : m_registrations )
        {
            registeration.unregister();
        }
        m_registrations.clear();
    }
}

Again, create the Content (extending DefaultContent makes it easy), set the 
DestinationID and call the register() method in the superlass.


And guess what? We are essentially done....

Ok, we need the FloorPanel and FranchiseePanel, which layouts everything to a 
panel. In our simple example, we just do;

public class FranchiseePanel extends Panel
    implements Serializable
{

    private static final long serialVersionUID = 1L;

    private static final String WICKET_ID_NAME_LABEL = "name";
    private static final String WICKET_ID_DESC_LABEL = "description";

    public FranchiseePanel( String id, Franchisee franchisee )
    {
        super( id );

        Label nameLabel = 
            new Label( WICKET_ID_NAME_LABEL, franchisee.getName() );
        add( nameLabel );

        Label descLabel = 
            new Label( WICKET_ID_DESC_LABEL, franchisee.getDescription() );
        add( descLabel );
    }
}

public class FloorPanel extends Panel
{

    public static final String WICKET_ID_NAME_LABEL = "name";
    private static final String WICKET_ID_FRANCHISEE = "franchisee";
    private static final String WICKET_ID_FRANCHISEES = "franchisees";

    public FloorPanel( String id, ContentContainer container, Floor floor )
    {
        super( id, new Model( floor.getName() ) );
        Label nameLabel = new Label( WICKET_ID_NAME_LABEL, floor.getName() );
        add( nameLabel );
        final List<Component> franchisees =
            container.createComponents( WICKET_ID_FRANCHISEE );
        if( franchisees.isEmpty() )
        {
            Panel p = new Panel( "franchisees" );
            p.add(new Label("franchisee","No Franchisees on this floor." ));
            add( p );
        }
        else
        {
            ListView listView = 
                new ListView( WICKET_ID_FRANCHISEES, franchisees )
            {
                protected void populateItem( final ListItem item )
                {
                    item.add( (Component) item.getModelObject() );
                }
            };
            add( listView );
        }
    }
}

The interesting bits sit in the 

final List<Component> franchisees = 
    container.createComponents( WICKET_ID_FRANCHISEE );

Here we are instructing the ContentContainer to create all its wicket children 
of a particular WicketID. This in turn will trigger the container to call all 
its children (Content) and if that Content is another ContentContainer it 
will be chained all the way to the Content leaves.

Voila!! The FloorPanels are built (note that it can create many components 
with the same ID). Then we add them into a ListView, which is standard Wicket 
stuff and beyond the scope here.


Ok, so far so good... but where are the Pages, and more importantly the Wicket 
Application, Servlets and all the other bits??

Well, first of all, we have not solved the "Page" issue yet, and so far the 
sample only works with the default HomePage. The rest of the bits are all in 
the Pax Wicket Service, i.e. nothing much you need to do. However, you need 
to define a PaxWicketApplicationFactory and typically you do this in the 
"application" bundle. The Activator for that looks something like;

public class Activator
    implements BundleActivator
{
    private ContentContainer m_store;
    private ServiceRegistration m_serviceRegistration;

    public void start( BundleContext bundleContext )
        throws Exception
    {
        IPageFactory factory = new IPageFactory()
        {
            public Page newPage( final Class pageClass )
            {
                return new OverviewPage( m_store, "Sungei Wang Plaza" );
            }

            public Page newPage( final Class pageClass, 
                                 final PageParameters parameters )
            {
                return new OverviewPage( m_store, "Sungei Wang Plaza" );
            }
        };
        m_store = new DefaultPageContainer( "swp", bundleContext, factory );
        Properties props = new Properties();
        props.put( PaxWicketApplicationFactory.MOUNTPOINT, "swp" );
        PaxWicketApplicationFactory applicationFactory = 
            new PaxWicketApplicationFactory( factory, OverviewPage.class );
        String serviceName = PaxWicketApplicationFactory.class.getName();
        m_serviceRegistration = bundleContext.registerService( 
            serviceName, applicationFactory, props );
    }

    public void stop( BundleContext bundleContext )
        throws Exception
    {
        m_serviceRegistration.unregister();
        m_store.dispose();
    }
}

The PaxWicketApplicationFactory needs the PageFactory and the Class of the 
HomePage. It is expected more things will be needed when we start solving how 
to support bookmarkable pages and such.
But essentially, the factory is plainly registered into the OSGi framework as 
a service and the Pax Wicket Service will use the mount point, create a 
servlet and deploy the Wicket application.

I expect we need another couple of days before the page part is completely 
solved.

For completeness, I also include the OverviewPage and its support class;

public class OverviewPage extends WebPage
{
    public static final int FLOOR_PAGE_SIZE = 10;
    public static final String WICKET_ID_LABEL = "storeName";

    public OverviewPage( ContentContainer container, String storeName )
    {
        Label label = new Label( WICKET_ID_LABEL, storeName );
        add( label );
        final List<Component> floors = container.createComponents( "floor" );
        List tabs = new ArrayList();
        for( final Component floor : floors )
        {
            String tabName = (String) floor.getModelObject();
            tabs.add( new AbstractTab( new Model( tabName ) )
            {
                public Panel getPanel( String panelId )
                {
                    Panel panel = new FloorTabPanel( panelId );
                    panel.add( floor );
                    return panel;
                }
            }
            );
        }
        if( tabs.isEmpty() )
        {
            add( new Label( "floors", "No Floors installed yet." ) );
        }
        else
        {
            add( new AjaxTabbedPanel( "floors", tabs ) );
        }
    }
}

public class FloorTabPanel extends Panel
{

    public FloorTabPanel( String id )
    {
        super( id );
    }
}


Ok, so there are still some outstanding issues;

 1. Wicket-extensions is using a scanning of Jars/Zips approach to locate the 
JavaScript for the UploadProgressBar, which fails due to the OSGi frameworks 
are not using standard URLs (zip/jar or file) for the resources, and hence 
not scannable. I have suggested to Wicket that the location of the JS in 
question is done with a direct reference, instead of the regexp. That should 
work, but I am not sure if there are some additional thoughts why the regexp 
has been chosen. We have had to disable the UploadProgressBar, as we use the 
wicket-extensions in our sample. 

 2. Wicket has a MarkupCache which will hold on to various resources. If it 
can't find it, it will store that information into the cache as well, so even 
if the resource becomes available, it will not be served in a later request. 
This can probably be solved simply by clearing the cache on each change of 
the OSGi service registrations, and doesn't require any changes to Wicket.

 3. We have not been able to fully understand the life cycle of a Page 
instance in Wicket, and hope for some expert advice on the matter. Dynamic 
changes to the component hierarchy will only occur when the page is created, 
hence the need to understand this in detail.

 4. There is an ambiguity in the OSGi specification of the HTTP service. Page 
17-432 in chapter 102.2 it says;
<quote>
Therefore, the same Servlet instance must not be reused for registration with 
another Http Service, nor can it be registered under multiple names. Unique 
instances are required for each registration.
</quote>
It is unclear to us, whether one is allowed to unregister the servlet, and 
then re-register the same servlet under a different mountpoint.
This ambiguity combined with a life cycle requirement in Wicket (which could 
be reviewed and fixed) we have been forced to create a new Servlet whenever 
the mountpoint is changed in PaxWicketApplicationFactory registration.
We consider this a somewhat acceptable limitation, and will not persue it 
further.

 5. Wicket supports the removal of components from containers, but we have no 
experience in what one is allowed and not allowed to do and "when" such can 
be done. As for now, we don't try to modify a created page in Wicket, but 
asking for any advice in the matter from experienced Wicket developers.



Ok, this was a fairly long mail, and I hope some of you are actually 
interested enough to read it all.



Cheers
Niclas


-------------------------------------------------------
Using Tomcat but need to do more? Need to support web services, security?
Get stuff done quickly with pre-integrated technology to make your job easier
Download IBM WebSphere Application Server v.1.0.1 based on Apache Geronimo
http://sel.as-us.falkag.net/sel?cmd=lnk&kid=120709&bid=263057&dat=121642
_______________________________________________
Wicket-develop mailing list
Wicket-develop@lists.sourceforge.net
https://lists.sourceforge.net/lists/listinfo/wicket-develop

Reply via email to