Properties Module

Last modified by Vincent Massol on 2024/07/05 17:47

cogProvides APIs to handle Java Beans properties and conversion between Java type and strings
TypeJAR
Category
Developed by

XWiki Development Team

Rating
0 Votes
LicenseGNU Lesser General Public License 2.1
Bundled With

XWiki Standard

Description

We previously used BeanUtils, for macros for example, but several needs were not covered by BeanUtils, so we started our own populate methods and the needed api around it. Basically the differences with BeanUtils are:

  • We needed to support any case for the properties names for macros
  • We needed to be able to inject custom fields in a bean for Wiki macros
  • We needed to support some annotation tweak in the bean description to make it easier to provide useful information, to the WYSIWYG for example, or to modify the behavior of the populate process, like making a property mandatory or hidden
  • BeanUtils does not support public fields (the Groovy way)
  • It's possible to implement a Converter component which can access anything an XWiki component can access so that you would be able to convert types like String into Document, etc.
  • Users are using only api/interface and any implementation component can be done for it.

Note that it's not a complete rewrite of BeanUtils. Xwiki-properties still use useful tools of javax.beans and ConvertUtils. The only things that have been completely rewritten are the populate method and the BeanInfo, that has been extended as a BeanDescriptor (to contain public fields, text description, etc.).

BeanManager

Populate a Java Bean

To populate a Map into a java bean use #populate(Object, Map<String, ? >).

By default a bean is parsed as follows:

  • java.beansIntrospector#getBeanInfo(Class<?>) is used to get standard BeanInfo properties which are based on getters and setters names. See BeanInfo javadoc.
  • Public fields produce also properties
  • Any case is supported for properties names when populating
  • Each property is converted on the fly from the provided value to the property JAVA type using ConverterManager.

Then it's possible to tweak this behavior with annotations:

  • org.xwiki.properties.annotation.PropertyDescription: a text describing the property
  • org.xwiki.properties.annotation.PropertyHidden: indicates that the property should not be taken into account by BeanManager
  • org.xwiki.properties.annotation.PropertyMandatory: indicates that an error should be generated when no value is provided in the Map for this property when populating.
  • org.xwiki.properties.annotation.PropertyName: a text with the display name of the field
  • since 6.3 org.xwiki.properties.annotation.PropertyId: overwrite the property identifier
  • since 10.10 org.xwiki.properties.annotation.PropertyAdvanced: indicates that the property is to be used by more advanced users
  • since 10.11 org.xwiki.properties.annotation.PropertyGroup: used to group properties together
  • since 10.11 org.xwiki.properties.annotation.PropertyFeature: binds a property to a feature (two properties can be used for the same feature)
  • since 11.0 org.xwiki.properties.annotation.PropertyDisplayType: overrides the type of the property for display only (e.g. WYSIWYG)
  • since 12.4RC1 org.xwiki.properties.annotation.PropertyDisplayHidden: indicates that the property should not be displayed in UIs (e.g. in the WYSIWYG macro editor)

Finally default implementation of BeanManager support JSR 303 (Bean Validation) if an implementation can be found.

Example

/**
 * Parameters for the {@link org.xwiki.rendering.internal.macro.include.IncludeMacro} Macro.
 *
 * @version $Id: 0c4d11f62ee64039a374207507deb3de19bd51a1 $
 * @since 1.6M1
 */

public class IncludeMacroParameters
{
   /**
     * @version $Id: 0c4d11f62ee64039a374207507deb3de19bd51a1 $
     */

   public enum Context
   {
       /**
         * Macro executed in its own context.
         */

        NEW,

       /**
         * Macro executed in the context of the current page.
         */

        CURRENT
   }

   /**
     * @see #getReference()
     */

   private String reference;

   /**
     * @see #getType()
     */

   private EntityType type = EntityType.DOCUMENT;

   /**
     * Defines whether the included page is executed in its separated execution context or whether it's executed in the
     * context of the current page.
     */

   private Context context = Context.CURRENT;

   /**
     * @see #getSection()
     */

   private String section;
   
   /**
     * @see #isExcludeFirstHeading()
     */

   private boolean excludeFirstHeading;
   
   /**
     * @param reference the reference of the resource to include
     * @since 3.4M1
     */

   @PropertyDescription("the reference of the resource to display")
   @PropertyDisplayType(EntityReferenceString.class)
   @PropertyFeature("reference")
   @PropertyGroup("stringReference")
   public void setReference(String reference)
   {
       this.reference = reference;
   }

   /**
     * @return the reference of the resource to include
     * @since 3.4M1
     */

   public String getReference()
   {
       return this.reference;
   }

   /**
     * @param sectionId see {@link #getSection()}
     */

   @PropertyDescription("an optional id of a section to include in the specified document, e.g. 'HMyHeading'")
   @PropertyAdvanced
   public void setSection(String sectionId)
   {
       this.section = sectionId;
   }

   /**
     * @return the optional id of a section to include in the referenced document. If not specified the whole document
     *         is included.
     */

   public String getSection()
   {
       return this.section;
   }

   /**
     * @param excludeFirstHeading {@code true} to remove the first heading found inside
     *        the document or the section, {@code false} to keep it
     * @since 12.4RC1
     */

   @Unstable
   @PropertyName("Exclude First Heading")
   @PropertyDescription("Exclude the first heading from the included document or section.")
   @PropertyAdvanced
   public void setExcludeFirstHeading(boolean excludeFirstHeading)
   {
       this.excludeFirstHeading = excludeFirstHeading;
   }
   
   /**
     * @return whether to exclude the first heading from the included document or section, or not.
     * @since 12.4RC1
     */

   @Unstable
   public boolean isExcludeFirstHeading()
   {
       return this.excludeFirstHeading;
   }

   /**
     * @param context defines whether the included page is executed in its separated execution context or whether it's
     *            executed in the context of the current page.
     */

   @PropertyDescription("defines whether the included page is executed in its separated execution context"
       + " or whether it's executed in the context of the current page")
   @PropertyAdvanced
   // Marked deprecated since there's now a Display macro instead.
   @Deprecated
   public void setContext(Context context)
   {
       this.context = context;
   }

   /**
     * @return defines whether the included page is executed in its separated execution context or whether it's executed
     *         in the context of the current page.
     */

   public Context getContext()
   {
       return this.context;
   }

   /**
     * @param type the type of the reference
     * @since 3.4M1
     */

   @PropertyDescription("the type of the reference")
   @PropertyGroup("stringReference")
   @PropertyAdvanced
   // Marking it as Display Hidden because it's complex and we don't want to confuse our users.
   @PropertyDisplayHidden
   public void setType(EntityType type)
   {
       this.type = type;
   }

   /**
     * @return the type of the reference
     * @since 3.4M1
     */

   public EntityType getType()
   {
       return this.type;
   }

   /**
     * @param page the reference of the page to include
     * @since 10.6RC1
     */

   @PropertyDescription("The reference of the page to include")
   @PropertyDisplayType(PageReference.class)
   @PropertyFeature("reference")
   // Display hidden because we don't want to confuse our users by proposing two ways to enter the reference to
   // include and ATM we don't have a picker for PageReference types and we do have a picker for EntityReference string
   // one so we choose to keep the other one visible and hide this one. We're keeping the property so that we don't
   // break backward compatibility when using the macro in wiki edit mode.
   @PropertyDisplayHidden
   public void setPage(String page)
   {
       this.reference = page;
       this.type = EntityType.PAGE;
   }
}
Map<String, String> values = new HashMap<String, String>();

values.put("reference", "Space.Page");
values.put("context", "new");
values.put("publicField", "42");
values.put("hiddenProperty", "hiddenPropertyvalue");

IncludeMacroParameters bean = new IncludeMacroParameters();

BeanManager beanManager = componentManager.getInstance(BeanManager.class);
beanManager.populate(bean, values);

assertEquals("Space.Page", bean.getReference());
assertEquals(Context.NEW, bean.getContext());
assertEquals(42, bean.publicField);
assertNull(bean.getHiddenProperty());

Get bean descriptor

It's possible to ask BeanManager descriptor generated from a Java Bean for easy listing of properties, values, etc. For example this is used to fill ParameterDescriptors of most of the Java-based macros.

ConverterManager

The default implementation of ConverterManager embed converters for the following types:

  • Any Apache ConvertUtils conversion
  • Any java.lang.Enum child type
  • java.util.Collection
  • java.util.List
  • java.util.ArrayList
  • java.util.Set
  • java.util.LinkedHashSet
  • java.util.HashSet
  • java.awt.Color
  • java.util.Locale
  • org.xwiki.rendering.syntax.Syntax
  • org.xwiki.component.namespace.Namespace

Convert a value in a target Java type

To convert a value you can use the default implementation of org.xwiki.properties.ConverterManager component interface and its convert method.

ConverterManager converterManager = componentManager.getInstance(ConverterManager.class);
Integer intValue = converterManager.convert(Integer.class, "42");

Add a new Converter

There are two ways to add support for new types.

Converter component (Recommended)

You can add a new converter by implementing org.xwiki.properties.converter.Converter component interface. It's recommended to extend org.xwiki.properties.converter.AbstractConverter, which provides some helper to create a new Converter.

The minimum for an AbstractConverter child is:

  • Implements #convertToType(java.lang.Class, java.lang.Object)
  • Indicate the supported type:
    • [On 5.2 and more] Indicate the type as AbstractConverter generic type
    • [Before 5.2] Set the type name as component role hint
/**
 * Bean Utils converter that converts a value into an {@link Color} object.
 */

@Component
@Singleton
public class ColorConverter extends AbstractConverter<Color>
{
   /**
     * The String input supported by this {@link org.apache.commons.beanutils.Converter}.
     */

   private static final String USAGE = "Color value should be in the form of '#xxxxxx' or 'r,g,b'";

   @Override
   protected Color convertToType(Type type, Object value)
   {
        Color color = null;
       if (value != null) {
            color = parse(value.toString());
       }

       return color;
   }

   @Override
   protected String convertToString(Color value)
   {
        Color colorValue = (Color) value;

       return MessageFormat.format("{0},{1},{2}", colorValue.getRed(), colorValue.getGreen(), colorValue.getBlue());
   }

   /**
     * Parsers a String in the form "x, y, z" into an SWT RGB class.
     *
     * @param value the color as String
     * @return RGB
     */

   protected Color parseRGB(String value)
   {
        StringTokenizer items = new StringTokenizer(value, ",");

       try {
           int red = 0;
           if (items.hasMoreTokens()) {
                red = Integer.parseInt(items.nextToken().trim());
           }

           int green = 0;
           if (items.hasMoreTokens()) {
                green = Integer.parseInt(items.nextToken().trim());
           }

           int blue = 0;
           if (items.hasMoreTokens()) {
                blue = Integer.parseInt(items.nextToken().trim());
           }

           return new Color(red, green, blue);
       } catch (NumberFormatException ex) {
           throw new ConversionException(value + "is not a valid RGB colo", ex);
       }
   }

   /**
     * Parsers a String in the form "#xxxxxx" into an SWT RGB class.
     *
     * @param value the color as String
     * @return RGB
     */

   protected Color parseHtml(String value)
   {
       if (value.length() != 7) {
           throw new ConversionException(USAGE);
       }

       int colorValue = 0;
       try {
            colorValue = Integer.parseInt(value.substring(1), 16);
           return new Color(colorValue);
       } catch (NumberFormatException ex) {
           throw new ConversionException(value + "is not a valid Html color", ex);
       }
   }

   /**
     * Convert a String in {@link Color}.
     *
     * @param value the String to parse
     * @return the {@link Color}
     */

   public Color parse(String value)
   {
       if (value.length() <= 1) {
           throw new ConversionException(USAGE);
       }

       if (value.charAt(0) == '#') {
           return parseHtml(value);
       } else if (value.indexOf(',') != -1) {
           return parseRGB(value);
       } else {
           throw new ConversionException(USAGE);
       }
   }
}

Apache ConvertUtils Converter

 
When convert manager can't find a specific converter for a type it uses Apache ConvertUtils. See http://commons.apache.org/beanutils/ for more details.

Get Connected