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
2
3
4
5
6
7
8
public class Article implements Serializable, Identifiable<Long>
{
public Long id;
public String title;
public String content;
public Date created;
public DeployerConfig deployerConfig;
}
and the DeployerConfig base class:
1
2
3
4
5
6
public abstract class DeployerConfig implements Identifiable<Long>, Serializable
{
public Long id;
public String pluginId;
public Article article;
}
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
2
3
4
5
6
7
public interface DeployerPlugin extends Identifiable<String>
{
String getName();
String deploy(Article article);
Component newEditor(String id, IModel< ? extends DeployerConfig> model);
DeployerConfig newConfig();
}
Our plugin interface extends Identifiable 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: 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: Armed with all this we are now ready to build the user interface. First we begin with a simple page to edit an Article: 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: 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. 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: 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: 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.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public class IdentifiableBeanRegistry<T extends Identifiable<ID>, ID extends Serializable>
implements
ApplicationContextAware,
InitializingBean
{
private final Class<T> beanType;
private ApplicationContext context;
private Map<ID, T> entries;
private List<T> beans;
private List<ID> uuids;
public IdentifiableBeanRegistry(Class<T> beanType)
{
this.beanType = beanType;
}
public void setApplicationContext(ApplicationContext context) throws BeansException
{
this.context = context;
}
public T getEntry(Serializable id)
{
T entry = entries.get(id);
if (entry == null)
{
throw new IllegalStateException();
}
return entry;
}
public List<T> getEntries()
{
return beans;
}
public List<ID> getIds()
{
return uuids;
}
@SuppressWarnings("unchecked")
public void afterPropertiesSet() throws Exception
{
// initialize the registry
entries = new HashMap<ID, T>();
final Map<String, ? extends T> matches = BeanFactoryUtils.beansOfTypeIncludingAncestors(
context, beanType);
for (T entry : matches.values())
{
entries.put(entry.getId(), entry);
}
entries = Collections.unmodifiableMap(entries);
// intialize indexes
beans = new ArrayList<T>(entries.values());
beans = Collections.unmodifiableList(beans);
uuids = new ArrayList<ID>(entries.keySet());
uuids = Collections.unmodifiableList(uuids);
}
}
The UI
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public abstract class EditArticlePage extends BasePage
{
public EditArticlePage(IModel<Article> article)
{
add(new FeedbackPanel("feedback"));
Form<Article> form = new TransactionalForm<Article>("form",
new CompoundPropertyModel<Article>(article))
{
@Override
protected void onSubmit()
{
onSave(getModelObject());
}
};
add(form);
form.add(new TextField<String>("title"));
form.add(new TextArea<String>("content"));
form.add(new Link<Void>("cancel")
{
@Override
public void onClick()
{
onCancel();
}
});
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public class DeployerPluginSelector extends Panel
{
@SpringBean
private DeployersRegistry registry;
public DeployerPluginSelector(String id, final IModel<Article> article)
{
super(id);
add(new WebMarkupContainer("editor"));
add(new PluginPicker("picker")
{
@Override
protected void onSelectionChanged(String pluginId)
{
DeployerPlugin plugin = registry.getEntry(pluginId);
article.getObject().setDeployerConfig(plugin.newConfig());
DeployerPluginSelector.this.replace(plugin.newEditor("editor",
new PropertyModel<DeployerConfig>(article, "deployerConfig")));
}
});
}
private static abstract class PluginPicker extends DropDownChoice<String>
{
@SpringBean
private DeployersRegistry registry;
public PluginPicker(String id)
{
super(id);
setRequired(true);
setModel(new Model<String>());
setChoices(new LoadableDetachableModel<List< ? extends String>>()
{
@Override
protected List< ? extends String> load()
{
return registry.getIds();
}
});
setChoiceRenderer(new IChoiceRenderer<String>()
{
public Object getDisplayValue(String object)
{
return registry.getEntry(object).getName();
}
public String getIdValue(String object, int index)
{
return object;
}
});
}
@Override
protected boolean wantOnSelectionChangedNotifications()
{
return true;
}
@Override
protected abstract void onSelectionChanged(String pluginId);
}
}
Implementing a Plugin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class HttpPostDeployerPlugin implements DeployerPlugin
{
public static final String ID = "plugin.deployer.httppost";
public String getName()
{
return "Http Post";
}
public String getId()
{
return ID;
}
public Component newEditor(String id, IModel< ? extends DeployerConfig> model)
{
return new HttpPostDeployerEditor(id, model);
}
public DeployerConfig newConfig()
{
return new HttpPostDeployerConfig();
}
public String deploy(Article article)
{
HttpPostDeployerConfig config = (HttpPostDeployerConfig)article.deployerConfig;
return "Deploying article: " article.title " to http://" config.url;
}
}
1
2
3
4
5
6
7
8
9
public class HttpPostDeployerConfig extends DeployerConfig
{
public HttpPostDeployerConfig()
{
pluginId = FtpDeployerPlugin.ID;
}
public String url;
}
1
2
3
4
5
6
7
8
public class HttpPostDeployerEditor extends Panel
{
public HttpPostDeployerEditor(String id, IModel< ? extends DeployerConfig> model)
{
super(id, new CompoundPropertyModel<DeployerConfig>(model));
add(new TextField<String>("url").setRequired(true));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private static class DeployArticleLink extends Link<Article>
{
@SpringBean
private DeployersRegistry registry;
private DeployArticleLink(String id, IModel<Article> model)
{
super(id, model);
}
@Override
public void onClick()
{
Article article = getModelObject();
DeployerPlugin plugin = registry.getEntry(article.deployerConfig.pluginId);
info(plugin.deploy(article));
}
}