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