Creating New XClass Property Types

Last modified by Vincent Massol on 2023/10/10

This tutorial guides you through the creation of a new XClass property type, which is a way to extend the class editor.

You should start by reading the XWiki Data Model to understand XWiki Classes, Objects and Properties, and then the Writing XWiki components tutorial because new property types are implemented as components.

Create a new property type

As an example, we will create an "External Image" property type which can be used to store URLs to external images. Let's start by creating the actual property type which must extend PropertyClass. Unfortunately this means that your Maven project will have to depend on XWiki's old core.

public class ExternalImageClass extends PropertyClass
{
   /**
     * Default constructor.
     */

   public ExternalImageClass()
   {
       // Specify the default name and pretty name of this XClass property. They can be overwritten from the class
       // editor when adding a property of this type to an XClass.
       super("externalImage", "External Image", null);
   }

   @Override
   public BaseProperty fromString(String value)
   {
        BaseProperty property = newProperty();
       // The stored value can be different than the value set by the user. You can do the needed transformations here.
       // In our case the value is an image URL so we keep it as it is. The reverse transformation, from the stored
       // value to the user friendly value, can be done in the property displayer.
       property.setValue(value);
       return property;
   }

   @Override
   public BaseProperty newProperty()
   {
       // The value of this XClass property is stored as a String. You have to use raw types here like StringProperty
       // because they are mapped to the database. Adding a new raw type implies modifying the Hibernate mapping and is
       // not the subject of this tutorial.
       BaseProperty property = new StringProperty();
        property.setName(getName());
       return property;
   }

   @Override
   public <T extends EntityReference> void mergeProperty(BaseProperty<T> currentProperty,
        BaseProperty<T> previousProperty, BaseProperty<T> newProperty, MergeConfiguration configuration,
        XWikiContext context, MergeResult mergeResult)
   {
       if (!Objects.equals(previousProperty, newProperty)) {
           if (Objects.equals(previousProperty, currentProperty)) {
                currentProperty.setValue(newProperty.getValue());
           } else {
               // Found conflict
               mergeResult.getLog().error("Collision found on property [{}] current has been modified", getName());
           }
       }
   }
}

Notice that we have used a StringProperty to store the value of our XClass property. The available raw property types that can be used for storing the value in the database are:

  • DateProperty
  • DBStringListProperty
  • DoubleProperty
  • FloatProperty
  • IntegerProperty
  • LargeStringProperty
  • LongProperty
  • StringListProperty
  • StringProperty

Extending this list is not possible without modifying the Hibernate mapping and is not the subject of this tutorial. You can create high level XClass property types but in the end their values will be stored as one of these raw types.

Also as you can see we have overwritten the default 3-way-merge implementation for this kind of properties to make sure the URL is not merged character by character but instead compared as a whole.

Next let's create a provider for our "External Image" property type. This is going to be used whenever a property of type "External Image" is added to an XClass (e.g. from the class editor).

@Component
// Note that the component hint matches the name of the property class without the "Class" suffix. The reason is that
// the component hint must match the value returned by the #getClassType() method of your property class, which by
// default strips the "Class" suffix from the Java class name of your property class. If you want to use a different
// hint that doesn't follow this naming convention you need to override #getClassType().
@Named("ExternalImage")
@Singleton
public class ExternalImageClassProvider implements PropertyClassProvider
{
   @Override
   public PropertyClassInterface getInstance()
   {
       return new ExternalImageClass();
   }

   @Override
   public PropertyMetaClassInterface getDefinition()
   {
        PropertyMetaClass definition = new PropertyMetaClass();
       // This text will appear in the drop down list of property types to choose from in the class editor.
       definition.setPrettyName("External Image");
        definition.setName(getClass().getAnnotation(Named.class).value());

       // Add a meta property that will allows us to specify a CSS class name for the image HTML element.
       // NOTE: We define meta properties using XClass property types. This means for instance that you can define meta
       // properties of External Image type or whatever XClass property type you create.
       StringClass styleName = new StringClass();
        styleName.setName("styleName");
        styleName.setPrettyName("Style Name");
        definition.safeput(styleName.getName(), styleName);

       // The alternative text is required for a valid image HTML element so we add a meta property for it.
       StringClass placeholder = new StringClass();
        placeholder.setName("placeholder");
        placeholder.setPrettyName("Alternative Text");
        definition.safeput(placeholder.getName(), placeholder);

       // Add more meta properties here.

       return definition;
   }
}

The provider acts like a factory for our property type but it also defines the list of meta properties. Each XClass property type has a list of meta properties that control how the property is displayed, how it's value is parsed, and so on. The values of these meta properties are shared by all instances of an XClass. So for instance, if you create a XClass with an "External Image" property and set, from the class editor, the "styleName" meta property to "icon" then all objects of that XClass will use that value.

The final step is to add the provider component to components.txt.

org.xwiki.example.internal.ExternalImageClassProvider

Now you can build your Maven project and copy the generated jar to the WEB-INF/lib folder of your XWiki instance. Restart the server and you're done.

Use the new property type

Let's create a class that has a property of type "External Image". You should see "External Image" listed in the drop down list box in the class editor.

addProperty.png

After you add the property to the class you can set all its meta properties. You'll notice that each property has a list of standard meta properties, like name or pretty name, and some specific meta properties, like style name and alternative text in our case.

editProperty.png

Save the class and let's add an object (instance) of this class to a wiki page. For this you have to edit the wiki page in object mode.

editObject.png

Save the page and let's create a sheet for our class.

{{velocity}}
; $doc.displayPrettyName('screenshot', false, false)
: $doc.display('screenshot')
{{/velocity}}

Now if you look at the wiki page that has the object you'll see that it doesn't look too good. This is because the "External Image" property type uses a default displayer that only displays the value (the image URL).

viewModeNoDisplayer.png

Write a displayer for the new property type

We can improve the display of our "External Image" properties by creating a custom displayer. One way to achieve this is by creating a Velocity template displayer_externalimage.vm under the /templates/ folder.

#if ($type == 'edit' || $type == 'search')
  #set ($id = $escapetool.xml("${prefix}${name}"))
  <input type="text" id="$!id" name="$!id" value="$!escapetool.xml($value)" />
#elseif ($type == 'view' || $type == 'rendered')
  <img src="$escapetool.xml($value)" alt="$escapetool.xml($field.getProperty('placeholder').value)"
    class="$escapetool.xml($field.getProperty('styleName').value)" />
#elseif ($type == 'hidden')
  #set ($id = $escapetool.xml("${prefix}${name}"))
  <input type="hidden" id="$!id" name="$!id" value="$!escapetool.xml($value)" />
#else
  ## In order for the custom displayer to be taken into account, the result of its evaluation with an unknown display
  ## mode must not be empty. Let's output something.
  Unknown display mode.
#end

You can read more about custom displayers. The wiki page should look better in view mode now.

viewMode.png

In edit mode it will look the same but you can extend the displayer to provide image preview for instance.

editMode.png

Get Connected