Properties Module

Version 31.1 by Thomas Mortagne on 2014/10/09 15:14

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 Enterprise, XWiki Enterprise Manager

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: indicate that the property should not be taken into account by BeanManager
  • org.xwiki.properties.annotation.PropertyMandatory: indicate 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

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

/**
 * Parameters for the {@link org.xwiki.rendering.internal.macro.include.IncludeMacro} Macro.
 */

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

        NEW,

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

        CURRENT;
   };

   /**
     * The name of the document to include.
     */

   private String 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;

   private String hiddenProperty;

   public int publicField;

   /**
     * @param document the name of the document to include.
     */

   @PropertyMandatory
   @PropertyDescription("the name of the document to include")
   public void setDocument(String document)
   {
       this.document = document;
   }

   /**
     * @return the name of the document to include.
     */

   public String getDocument()
   {
       return this.document;
   }

   /**
     * @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")
   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;
   }

   @PropertyHidden
   public void setHiddenProperty(String hiddenProperty)
   {
     this.hiddenProperty = hiddenProperty;
   }

   public String getHiddenProperty()
   {
     return this.hiddenProperty;
   }
}
Map<String, String> values = new HashMap<String, String>();

values.put("document", "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.getDocument());
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

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.

Tags: development
    

Get Connected