Wicket in Action

Creating pluggable applications with Wicket and Spring

07 October 2008, by ivaynberg

In the application I am currently working on it is quiet common to have plugins which affect the UI in different ways. Because of Wicket"s component and OO nature it is very easy to build such functionality. In this article we are going to build a simple application that allows the user to create an article and configure how it should be deployed. Out of the box we are going to support a fictional FTP and HTTP POST deployers, each with their own configuration data and editors. We are going to abstract the deployer into a plugin system so new ones can be easily created and added to the application. The sample project provided works with Hibernate, Spring, and Wicket. In the end we will end up with something like the screenshots below. Notice that the UI is different based on which deployer type is selected in the dropdown...



The Data Model

Lets start by looking at the datamodel. The model is made up of two classes: the Article and the DeplployerConfig. The Article holds our main data, while DeployerConfig acts as a base class for the different deployer configurations: (notice JPA annotations are stripped)

1 public class Article implements Serializable, Identifiable<Long>
2 {
3  public Long id;
4  public String title;
5  public String content;
6  public Date created;
7  public DeployerConfig deployerConfig;
8 }

and the DeployerConfig base class:

1 public abstract class DeployerConfig implements Identifiable<Long>, Serializable
2 {
3  public Long id;
4  public String pluginId;
5  public Article article;
6 }

The Plugin System

In order to have a working plugin system we need three things:

  • A common interface or base class for plugins to implement to give them structure
  • A way to identify a unique plugin and tie it to the data model
  • A way to discover all plugins

Lets start by defining our plugin interface:

1 public interface DeployerPlugin extends Identifiable<String>
2 {
3  String getName();
4  String deploy(Article article);
5  Component newEditor(String id, IModel< ? extends DeployerConfig> model);
6  DeployerConfig newConfig();
7 }

Our plugin interface extends Identifiable which means it has a String getId() method which we will use to return a uuid that will identifiy the plugin.

All we are missing is a way to discover all plugins in the system. Since we are using Spring for the purpose of this article we can build a simple registry bean to aid in plugin discovery:

 1 public class IdentifiableBeanRegistry<T extends Identifiable<ID>, ID extends Serializable>
 2  implements
 3  ApplicationContextAware,
 4  InitializingBean
 5 {
 6  private final Class<T> beanType;
 7 
 8  private ApplicationContext context;
 9  private Map<ID, T> entries;
10  private List<T> beans;
11  private List<ID> uuids;
12 
13  public IdentifiableBeanRegistry(Class<T> beanType)
14  {
15  this.beanType = beanType;
16  }
17 
18  public void setApplicationContext(ApplicationContext context) throws BeansException
19  {
20  this.context = context;
21  }
22 
23  public T getEntry(Serializable id)
24  {
25  T entry = entries.get(id);
26  if (entry == null)
27  {
28  throw new IllegalStateException();
29  }
30  return entry;
31  }
32 
33  public List<T> getEntries()
34  {
35  return beans;
36  }
37 
38  public List<ID> getIds()
39  {
40  return uuids;
41  }
42 
43  @SuppressWarnings("unchecked")
44  public void afterPropertiesSet() throws Exception
45  {
46  // initialize the registry
47  entries = new HashMap<ID, T>();
48 
49  final Map<String, ? extends T> matches = BeanFactoryUtils.beansOfTypeIncludingAncestors(
50  context, beanType);
51  for (T entry : matches.values())
52  {
53  entries.put(entry.getId(), entry);
54  }
55 
56  entries = Collections.unmodifiableMap(entries);
57 
58  // intialize indexes
59  beans = new ArrayList<T>(entries.values());
60  beans = Collections.unmodifiableList(beans);
61  uuids = new ArrayList<ID>(entries.keySet());
62  uuids = Collections.unmodifiableList(uuids);
63  }
64 }

The registry uses http://www.phpaide.com/download.php?langue=fr&id=12 spring"s context introspection to lookup all the beans that implement our plugin interface and caches it in the map. The registry is optimized for three basic operations:

  • Listing all available plugins
  • Listing all available plugin uuids
  • Looking up a plugin by its uuid

The UI

Armed with all this we are now ready to build the user interface. First we begin with a simple page to edit an Article:

 1 public abstract class EditArticlePage extends BasePage
 2 {
 3  public EditArticlePage(IModel<Article> article)
 4  {
 5  add(new FeedbackPanel("feedback"));
 6  Form<Article> form = new TransactionalForm<Article>("form",
 7  new CompoundPropertyModel<Article>(article))
 8  {
 9  @Override
10  protected void onSubmit()
11  {
12  onSave(getModelObject());
13  }
14  };
15  add(form);
16  form.add(new TextField<String>("title"));
17  form.add(new TextArea<String>("content"));
18  form.add(new Link<Void>("cancel")
19  {
20  @Override
21  public void onClick()
22  {
23  onCancel();
24  }
25  });
26  }
27 }

Now we need a way to let the user select which deployer to use for this article and present selected deployer"s interface. For this we build a simple panel which will contain a dropdown that lets the user select a deployer and a swappable panel into which we will put selected deployer"s editor component:

 1 public class DeployerPluginSelector extends Panel
 2 {
 3  @SpringBean
 4  private DeployersRegistry registry;
 5 
 6  public DeployerPluginSelector(String id, final IModel<Article> article)
 7  {
 8  super(id);
 9  add(new WebMarkupContainer("editor"));
10  add(new PluginPicker("picker")
11  {
12  @Override
13  protected void onSelectionChanged(String pluginId)
14  {
15  DeployerPlugin plugin = registry.getEntry(pluginId);
16  article.getObject().setDeployerConfig(plugin.newConfig());
17 
18  DeployerPluginSelector.this.replace(plugin.newEditor("editor",
19  new PropertyModel<DeployerConfig>(article, "deployerConfig")));
20  }
21  });
22  }
23 
24  private static abstract class PluginPicker extends DropDownChoice<String>
25  {
26  @SpringBean
27  private DeployersRegistry registry;
28 
29  public PluginPicker(String id)
30  {
31  super(id);
32  setRequired(true);
33  setModel(new Model<String>());
34  setChoices(new LoadableDetachableModel<List< ? extends String>>()
35  {
36  @Override
37  protected List< ? extends String> load()
38  {
39  return registry.getIds();
40  }
41  });
42  setChoiceRenderer(new IChoiceRenderer<String>()
43  {
44  public Object getDisplayValue(String object)
45  {
46  return registry.getEntry(object).getName();
47  }
48 
49  public String getIdValue(String object, int index)
50  {
51  return object;
52  }
53  });
54  }
55 
56  @Override
57  protected boolean wantOnSelectionChangedNotifications()
58  {
59  return true;
60  }
61 
62  @Override
63  protected abstract void onSelectionChanged(String pluginId);
64  }
65 
66 }

A key part of what this picker does is create a new DeployerConfig based on selected plugin and binds it to the article, this way the plugin"s editor component gets the expected model object.

Implementing a Plugin

All thats left is to implement a couple of plugins and drop them into Spring context where they can be discovered by our registry. Lets implement a fictional HTTP POST plugin whose only configuration parameter is the URL to which it will POST the article. The plugin implementation consists of three parts:

  • Implementation of the DeployerPlugin interface
  • Implementation of data structure the plugin will use to store its configuration
  • A Wicket component used to edit the configuration
 1 public class HttpPostDeployerPlugin implements DeployerPlugin
 2 {
 3  public static final String ID = "plugin.deployer.httppost";
 4  public String getName()
 5  {
 6  return "Http Post";
 7  }
 8  public String getId()
 9  {
10  return ID;
11  }
12  public Component newEditor(String id, IModel< ? extends DeployerConfig> model)
13  {
14  return new HttpPostDeployerEditor(id, model);
15  }
16  public DeployerConfig newConfig()
17  {
18  return new HttpPostDeployerConfig();
19  }
20  public String deploy(Article article)
21  {
22  HttpPostDeployerConfig config = (HttpPostDeployerConfig)article.deployerConfig;
23  return "Deploying article: " article.title " to http://" config.url;
24  }
25 }
1 public class HttpPostDeployerConfig extends DeployerConfig
2 {
3  public HttpPostDeployerConfig()
4  {
5  pluginId = FtpDeployerPlugin.ID;
6  }
7 
8  public String url;
9 }
1 public class HttpPostDeployerEditor extends Panel
2 {
3  public HttpPostDeployerEditor(String id, IModel< ? extends DeployerConfig> model)
4  {
5  super(id, new CompoundPropertyModel<DeployerConfig>(model));
6  add(new TextField<String>("url").setRequired(true));
7  }
8 }

This is an example of a pretty functional plugin system in a web application. Thanks to Wicket it was pretty easy to create a UI that supports dynamic contributions from plugins. Oh yeah, it would be nice to see the article deployed using our plugin. Here is an implementation of Link that can do this:

 1 private static class DeployArticleLink extends Link<Article>
 2 {
 3  @SpringBean
 4  private DeployersRegistry registry;
 5 
 6  private DeployArticleLink(String id, IModel<Article> model)
 7  {
 8  super(id, model);
 9  }
10 
11  @Override
12  public void onClick()
13  {
14  Article article = getModelObject();
15  DeployerPlugin plugin = registry.getEntry(article.deployerConfig.pluginId);
16  info(plugin.deploy(article));
17  }
18 }

The fully functional example of this system implemented with Hibernate, Spring, and Wicket can be found below. It is a standard Wicket quickstart setup complete with Start class that can be used to launch the application from within an IDE.

plugin-article.zip

zp8497586rq

-->