Wiki Component API

Last modified by Admin on 2024/04/26 11:12

cogMake it possible to implement a component in a wiki page, using wiki objects
TypeJAR
Category
Developed by

XWiki Development Team

Rating
0 Votes
LicenseGNU Lesser General Public License 2.1
Bundled With

XWiki Standard

Installable with the Extension Manager

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:

  1. Write components directly within documents, using XObjects. Those compoments will be considered similar to components written in Java by the rest of the platform.
  2. 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.
  3. Instantiate components directly through XObjects.

A more recent alternative which bring much better performances and reliability is to use Script Components.

Write components in documents

Programming rights are required in order to write components in wiki 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

When we refer to Role Types, they can be:

  • Simple types, for example: org.xwiki.query.QueryManager
  • Parameterized types, like: org.xwiki.model.reference.EntityReferenceSerializer<java.lang.String>
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:

Listener1.png

If you look at the logs you should see the following error:

org.xwiki.component.wiki.WikiComponentRuntimeException: You need to add an Object of type [XWiki.ComponentMethodClass] in
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:

  1. xcontext.method.input, a Map<Integer, Object> of the arguments passed to the implemented method
  2. xcontext.method.output, an Object that you must be set if the implemented method returns a value
  3. xcontext.method.component, a reference to this component (for calling a method of it from another one)
  4. 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: 

Listener2.png

You now have a component implemented within a wiki page, but if you look at the log you should see something similar to this:

WARN  .o.i.DefaultObservationManager - The [$Proxy47] listener has overwritten a previously registered listener [$Proxy42]
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: 

Listener3.png

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:

Listener4.png

Now, since we're implementing this, we need to implement the only method from this interface, dispose():

{{groovy}}
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:

Listener5.png

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:

Hello world listener unregistered, it will now be registered again

You can download the complete example: Main.Listener.xar.

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();
}

View on github

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;
}

View on github

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.

package org.xwiki.example;

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:

package org.xwiki.example.internal;

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:

package org.xwiki.example.internal;

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:

  1. Create the XWiki.Proverb class, by adding a String property named proverb to it

    WikiComponents-Step1.png

  2. Create objects from that class, in one or multiple documents

    WikiComponents-Step2.png

  3. 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}}
  4. You'll see the proverbs you created appear

    WikiComponents-Step3.png

A real-life example

A real life example of this can be found in the UI Extension module:

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 16.2.0):

Get Connected