Wiki Component API
Make it possible to implement a component in a wiki page, using wiki objects |
Type | JAR |
Category | |
Developed by | |
Rating | |
License | GNU Lesser General Public License 2.1 |
Bundled With | XWiki Standard |
Table of contents
Description
Introduction
Introduced in XWiki 4.2, the wiki component module is a bridge between XWiki java components and wiki documents. The module has 3 features:
- Write components directly within documents, using XObjects. Those compoments will be considered similar to components written in Java by the rest of the platform.
- Easily bind a java component to a document through a mechanism re-instantiating the java component each time the corresponding document is modified. This allows the component to rely on information or even scripts stored in the wiki.
- Instantiate components directly through XObjects.
Write components in documents
It is possible to write components in wiki documents, using XObjects. This is not the preferred way to write a component but this mechanism can be used to make experiments on a running XWiki instance for example. Four different XClasses allow to define components:
- XWiki.ComponentClass
- Allows to mark that the document holds a component
- XWiki.ComponentMethodClass
- Allows to implement component methods with wiki syntax
- XWiki.ComponentDependencyClass
- Allows to have other components injected in the context when components methods are executed
- XWiki.ComponentInterfaceClass
- Allows to implement other interfaces in addition to the component interface
Defining the component
First we need to choose a Component role to implement, in this tutorial we will implement an Event Listener, which allows to execute code after some events are fired.
- Component Role Type
- The Role (Interface) the component implements, in our example org.xwiki.observation.EventListener
- Component Role Hint
- The Hint of your component, it must allow to identify it, in our example helloworld
- Component Scope
- The Scope of your component, it can be registered at 3 different level: only for the current wiki (default value), global (for a whole wiki farm) or only for the current user (the user who wrote the component)
This is what you should have:
If you look at the logs you should see the following error:
document [xwiki:Main.Listener] to implement method [org.xwiki.observation.EventListener.getName]
This is normal since we haven't implemented the component methods, yet.
Implementing methods
To implement methods we need to add one XWiki.ComponentMethodClass XObject per method. For an Event Listener we need to implement getEvents(), getName() and onEvent(Event, Object, Object)
We will implement those methods using XWiki Syntax, which allows us to use scripting languages such as velocity or groovy.
To interact with the outside world the methods are provided with some special binding variables grouped under the "method" context:
- xcontext.method.input, a Map<Integer, Object> of the arguments passed to the implemented method
- xcontext.method.output, an Object that you must be set if the implemented method returns a value
- xcontext.method.component, a reference to this component (for calling a method of it from another one)
- xcontext.method.<dependency binding name>, reference to inject dependencies (see "Adding dependencies" below)
Here's the code we need to implement our Event Listener:
- Method: getEvents{{groovy}}
import org.xwiki.bridge.event.*
xcontext.method.output.value = [new DocumentCreatedEvent(), new DocumentUpdatedEvent()]
{{/groovy}} - Method: onEvent{{groovy}}
System.out.println("Hello World ! The document ${xcontext.method.input.get(1)} has been created/modified.")
{{/groovy}} - Method: getName{{groovy}}
xcontext.method.output.value = "helloworld"
{{/groovy}}
This is what you should have:
You now have a component implemented within a wiki page, but if you look at the log you should see something similar to this:
since they both are registered under the same id [helloworld]. In the future consider removing a Listener first if you really want to register it again.
That's normal, every time you save the document XWiki tries to register the component as an Event Listener, but it needs to remove the previous one first, there's something we can do about that.
Adding dependencies
Like Java components our Wiki components can declare dependencies, those dependencies are injected in the method context (xcontext.method). In our current example we need to retrieve the Observation Manager to be able to un-register ourselves from it.
To do that, add a XWiki.ComponentDependencyClass XObject to your document and fill the object with the following information:
- Dependency Role Type
- The Role (Interface) the component implements, in our example org.xwiki.observation.ObservationManager
- Dependency Role Hint
- The Hint of the dependency, in our example default
- Binding name
- The name of the variable that will be put in our context to access the component, in our example observationManager
This is what you should have:
We'll now use that from a new method, see below.
Implementing other interfaces
To implement another interface, add a XWiki.ComponentInterfaceClass XObject to your document and fill the object to fit our needs. Here we will use this to implement org.xwiki.component.phase.Disposable. This will allow us to unregister the listener when the component is unloaded.
This is what you should have:
Now, since we're implementing this, we need to implement the only method from this interface, dispose():
System.out.println("Hello world listener unregistered, it will now be registered again")
xcontext.method.observationManager.removeListener("helloworld")
{{/groovy}}
This is what it should look like:
And this time we're done, our Listener will print a line every time a document is created or modified in the wiki, and every time you'll save the document holding the component you should see this in the log:
You can download the complete example:
.In case you need to perform some logging from such a WikiComponent, the logging ScriptService can be used to do so.
Bind component implementations to documents
When implementations of a component are defined (or at least of part of them) in documents what we did was:
- Search for all the implementations within the wiki, usually through a query looking for XObjects of a specific XClass.
- Create a component descriptor for each implementation found
- Register each implementation
- Set up a listener, to listen to:
- document creations and modifications, to unregister and register the implementations they contain, if any
- document deletions, to unregister implementations they contain, if any
The wiki component modules removes the need for some of those steps, to use it your component role must extend the WikiComponent Interface
* Represents the definition of a wiki component implementation. A java component can extend this interface if it needs
* to be bound to a document, in order to be unregistered and registered again when the document is modified, and
* unregistered when the document is deleted.
*
* @version $Id: 406ebb4a913d7bbe9cb5f2297152c9afd9efc9a2 $
* @since 4.2M3
*/
public interface WikiComponent
{
/**
* Get the reference of the document this component instance is bound to.
*
* @return the reference to the document holding this wiki component definition.
*/
DocumentReference getDocumentReference();
/**
* @return the role implemented by this component implementation.
*/
public Type getRoleType();
/**
* @return the hint of the role implemented by this component implementation.
*/
String getRoleHint();
}
Once your component extends the Interface above you need to provide a component builder implementing the following interface:
* Allows to provide a list of documents holding one or more {@link WikiComponent}, and to build components from those
* documents.
*
* @version $Id: 9d1ae83dc1970909a991ccf0a216809c0ddee30a $
* @since 4.2M3
*/
@Role
public interface WikiComponentBuilder
{
/**
* Get the list of documents holding components.
*
* @return the list of documents holding components
*/
List<DocumentReference> getDocumentReferences();
/**
* Build the components defined in a document XObjects. Being able to define more than one component in a document
* depends on the implementation. It is up to the implementation to determine if the last author of the document
* has the required permissions to register a component.
*
* @param reference the reference to the document that holds component definition objects
* @return the constructed component definition
* @throws WikiComponentException when the document contains invalid component definition(s)
*/
List<WikiComponent> buildComponents(DocumentReference reference) throws WikiComponentException;
}
When this is done, every time a document holding information about one of your component implementations is modified #buildComponents(DocumentReference) will be called, allowing you to rebuild the component bound to it. You will find an example of implementation below.
Example of WikiComponent implementation
We will make a very simple component for this example, Proverb. As seen above, this component Role extends WikiComponent.
import org.xwiki.component.annotation.Role;
import org.xwiki.component.wiki.WikiComponent;
@Role
public interface Proverb extends WikiComponent
{
/**
* @return A proverb
*/
String get();
}
We write a class implementing this new Role, this will be the bridge between components and the wiki:
import java.lang.reflect.Type;
import org.xwiki.component.wiki.WikiComponentScope;
import org.xwiki.example.Proverb;
import org.xwiki.model.reference.DocumentReference;
public class WikiProverb implements Proverb
{
private DocumentReference reference;
private DocumentReference authorReference;
private String proverb;
private String hint;
public WikiProverb(DocumentReference reference, DocumentReference authorReference, String hint, String proverb)
{
this.reference = reference;
this.authorReference = reference;
this.hint = hint;
this.proverb = proverb;
}
@Override
public String get()
{
return proverb;
}
@Override
public DocumentReference getDocumentReference()
{
return reference;
}
@Override
public DocumentReference getAuthorReference()
{
return authorReference;
}
@Override
public Type getRoleType()
{
return Proverb.class;
}
@Override
public String getRoleHint()
{
return hint;
}
@Override
public WikiComponentScope getScope()
{
return WikiComponentScope.WIKI;
}
}
And now we need a builder:
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import org.xwiki.component.annotation.Component;
import org.xwiki.component.wiki.WikiComponent;
import org.xwiki.component.wiki.WikiComponentBuilder;
import org.xwiki.component.wiki.WikiComponentException;
import org.xwiki.context.Execution;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.model.reference.EntityReferenceSerializer;
import org.xwiki.query.Query;
import org.xwiki.query.QueryManager;
import com.xpn.xwiki.XWikiContext;
import com.xpn.xwiki.doc.XWikiDocument;
import com.xpn.xwiki.objects.BaseObject;
@Component
@Singleton
@Named("proverb")
public class WikiProverbBuilder implements WikiComponentBuilder
{
@Inject
private Execution execution;
@Inject
private QueryManager queryManager;
@Inject
private EntityReferenceSerializer<String> serializer;
@Override
public List<DocumentReference> getDocumentReferences()
{
List<DocumentReference> references = new ArrayList<DocumentReference>();
try {
Query query =
queryManager.createQuery("select doc.space, doc.name from Document doc, doc.object(XWiki.Proverb) "
+ "as proverb where proverb.proverb <> ''",
Query.XWQL);
List<Object[]> results = query.execute();
for (Object[] result : results) {
references.add(
new DocumentReference(getXWikiContext().getDatabase(), (String) result[0], (String) result[1]));
}
} catch (Exception e) {
// Fail "silently"
e.printStackTrace();
}
return references;
}
@Override
public List<WikiComponent> buildComponents(DocumentReference reference) throws WikiComponentException
{
List<WikiComponent> components = new ArrayList<WikiComponent>();
DocumentReference proverbXClass = new DocumentReference(getXWikiContext().getDatabase(), "XWiki", "Proverb");
try {
XWikiDocument doc = getXWikiContext().getWiki().getDocument(reference, getXWikiContext());
if (!getXWikiContext().getWiki().getRightService().hasAccessLevel("admin", doc.getAuthor(),
"XWiki.XWikiPreferences", getXWikiContext())) {
throw new WikiComponentException(String.format("Failed to building Proverb components from document "
+" [%s], author [%s] doesn't have admin rights in the wiki", reference.toString(),
doc.getAuthor()));
}
for (BaseObject obj : doc.getXObjects(proverbXClass)) {
String roleHint = serializer.serialize(obj.getReference());
components.add(new WikiProverb(reference, doc.getAuthorReference(), roleHint,
obj.getStringValue("proverb")));
}
} catch (Exception e) {
throw new WikiComponentException(String.format("Failed to build Proverb components from document [%s]",
reference.toString()), e);
}
return components;
}
private XWikiContext getXWikiContext()
{
return (XWikiContext) this.execution.getContext().getProperty("xwikicontext");
}
}
Voila! Once the JAR is dropped within xwiki/WEB-INF/lib/ it's possible to create components from the wiki, to do so you need to:
- Create the XWiki.Proverb class, by adding a String property named proverb to it
- Create objects from that class, in one or multiple documents
- You can now write a little script to retrieve your components{{groovy}}
import org.xwiki.example.Proverb;
for (Proverb proverb : services.component.getComponentManager().getInstanceList(Proverb.class)) {
println("* " + proverb.get())
}
{{/groovy}} - You'll see the proverbs you created appear
A real-life example
A real life example of this can be found in the UI Extension module:
- The component : UIExtension
- The "bridge" implementation : WikiUIExtension
- The builder : WikiUIExtensionComponentBuilder
Instantiate components in documents
Since XWiki 9.5RC1, the Wiki Components API offers a new WikiObjectComponentBuilder interface that can be used to allow custom XObjects to instantiate different components implementing the WikiComponent interface.
Components that can be instantiated out of XObjects are directly registered against the Component Manager.
Allow a new component to be instantiated through XObjects
In order to use this API, you firstly have to create a new component with the WikiObjectComponentBuilder role. The hint of this component should be defined as the path of the XClass that you want to use with this builder. Every time an XObject implementing the specified XClass is added, updated, or deleted from the wiki, the builder will be in charge of extracting the necessary informations it needs from the XObject and instantiating one or more WikiComponent. The returned WikiComponent(s) are then registered against the Component Manager using a scope that is defined in each instantiated component through WikiComponent#getScope.
Note that :
- As it’s the builder responsibility to instantiate the correct WikiComponent(s), it should also be in charge to determine if the XObject author has the sufficient rights to instantiate such components, security checks should then be made on the builder side.
- By specifying a local or an absolute XClass path in the hint of your WikiObjectComponentBuilder, you can allow the components to be built out of XObjects present in a single Wiki (with an absolute path) or out of every XObjects implementing this particular XClass in the farm (when using a local path).
Prerequisites & Installation Instructions
We recommend using the Extension Manager to install this extension (Make sure that the text "Installable with the Extension Manager" is displayed at the top right location on this page to know if this extension can be installed with the Extension Manager).
You can also use the manual method which involves dropping the JAR file and all its dependencies into the WEB-INF/lib folder and restarting XWiki.
Dependencies
Dependencies for this extension (org.xwiki.platform:xwiki-platform-component-wiki 14.9):
- org.apache.commons:commons-lang3 3.12.0
- org.xwiki.platform:xwiki-platform-model-api 14.9
- org.xwiki.commons:xwiki-commons-component-api 14.9
- org.xwiki.rendering:xwiki-rendering-api 14.9
- org.xwiki.platform:xwiki-platform-bridge 14.9
- org.xwiki.platform:xwiki-platform-oldcore 14.9
- org.xwiki.platform:xwiki-platform-rendering-async-default 14.9