FELIX-4357: Support types beside String/String[] in @Property annotation.


git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@1554080 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/dependencymanager/annotation/src/main/java/org/apache/felix/dm/annotation/api/Property.java b/dependencymanager/annotation/src/main/java/org/apache/felix/dm/annotation/api/Property.java
index 5c589b5..f760ee9 100644
--- a/dependencymanager/annotation/src/main/java/org/apache/felix/dm/annotation/api/Property.java
+++ b/dependencymanager/annotation/src/main/java/org/apache/felix/dm/annotation/api/Property.java
@@ -24,13 +24,45 @@
 import java.lang.annotation.Target;
 
 /**
- * Annotation used to describe a property key-value pair. It is used when
- * declaring {@link Component#properties()} attribute.
- * This annotation only support properties which type is either a
- * String, or a String array.
- * If you need to specify some properties with types other than Strings,
- * then use the {@link Start} annotation, which allows to annotate a <code>start</code>
- * method which may optionally return a Map of any service property types.
+ * Annotation used to describe a property key-value(s) pair. It is used for example when
+ * declaring {@link Component#properties()} attribute.<p>
+ * 
+ * Property value(s) type is String by default, and the type is scalar if the value is single-valued, 
+ * or an array if the value is multi-valued.
+ * 
+ * Eight primitive types are supported:<p>
+ * <ul>
+ * <li> String (default type)
+ * <li> Long
+ * <li> Double
+ * <li> Float
+ * <li> Integer
+ * <li> Byte
+ * <li> Boolean
+ * <li> Short
+ * </ul>
+ * 
+ * You can specify the type of a property either using a combination of <code>value</code> and <code>type</code> attributes,
+ * or using one of the <code>longValue/doubleValue/floatValue/intValue/byteValue/charValue/booleanValue/shortValue</code> attributes.
+ * 
+ * Notice that you can also specify service properties dynamically by returning a Map from a method
+ * annotated with {@link Start}.
+ * 
+ * <p>
+ * <h3>Usage Examples</h3>
+ * <blockquote>
+ * 
+ * <pre>
+ * &#64;Component(properties={
+ *     &#64;Property(name="p1", value="v")})                    // String value type (scalar)
+ *     &#64;Property(name="p2", value={"s1", "s2")})            // Array of Strings
+ *     &#64;Property(name="service.ranking", intValue=10)       // Integer value type (scalar)
+ *     &#64;Property(name="p3", intValue={1,2})                 // Array of Integers
+ *     &#64;Property(name="p3", value={"1"), type=Long.class})  // Long value (scalar)
+ * class ServiceImpl implements Service {
+ * }
+ * </pre>
+ * </blockquote>
  * 
  * @author <a href="mailto:dev@felix.apache.org">Felix Project Team</a>
  */
@@ -45,14 +77,70 @@
     String name();
 
     /**
-     * Returns the property value
-     * @return this property value
+     * Returns the property value(s). The property value(s) is (are) 
+     * parsed using the <code>valueOf</code> method of the class specified in the #type attribute 
+     * (which is <code>String</code> by default). When the property value is single-value, then 
+     * the value type is scalar (not an array). If the property value is multi-valued, then the value type 
+     * is an array of the type specified in the {@link #type()} attribute (String by default).
+     * 
+     * @return this property value(s).
      */
-    String value() default "";
+    String[] value() default {};
     
     /**
-     * Returns the property value as a String array.
-     * @return this property value as a String array
+     * Specifies how the {@link #value()} or {@link #values()} attributes are parsed.
+     * @return the property value type (String by default) used to parse {@link #value()} or {@link #values()} 
+     * attribtues
      */
-    String[] values() default {};
+    Class<?> type() default String.class;    
+    
+    /**
+     * A Long value or an array of Long values. 
+     */
+    long[] longValue() default {};
+
+    /**
+     * A Double value or an array of Double values. 
+     */
+    double[] doubleValue() default {};
+
+    /**
+     * A Float value or an array of Float values. 
+     */
+    float[] floatValue() default {};
+
+    /**
+     * An Integer value or an array of Integer values. 
+     */
+    int[] intValue() default {};
+
+    /**
+     * A Byte value or an array of Byte values. 
+     */
+    byte[] byteValue() default {};
+
+    /**
+     * A Character value or an array of Character values. 
+     */
+    char[] charValue() default {};
+
+    /**
+     * A Boolean value or an array of Boolean values. 
+     */
+    boolean[] booleanValue() default {};
+
+    /**
+     * A Short value or an array of Short values. 
+     */
+    short[] shortValue() default {};
+    
+    /**
+     * Returns an array of property values.
+     * The property value are parsed using the <code>valueOf</code> method of the class specified in the #type attribute 
+     * (which is <code>String</code> by default).
+     * 
+     * @return an array of property values. 
+     * @deprecated use {@link #value()} attribute.
+     */
+    String[] values() default {};    
 }
diff --git a/dependencymanager/annotation/src/main/java/org/apache/felix/dm/annotation/plugin/bnd/AnnotationCollector.java b/dependencymanager/annotation/src/main/java/org/apache/felix/dm/annotation/plugin/bnd/AnnotationCollector.java
index 6a237c1..a17f3ec 100644
--- a/dependencymanager/annotation/src/main/java/org/apache/felix/dm/annotation/plugin/bnd/AnnotationCollector.java
+++ b/dependencymanager/annotation/src/main/java/org/apache/felix/dm/annotation/plugin/bnd/AnnotationCollector.java
@@ -21,10 +21,8 @@
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
 
 import org.apache.felix.dm.annotation.api.AdapterService;
@@ -39,20 +37,23 @@
 import org.apache.felix.dm.annotation.api.Init;
 import org.apache.felix.dm.annotation.api.Inject;
 import org.apache.felix.dm.annotation.api.LifecycleController;
+import org.apache.felix.dm.annotation.api.Registered;
 import org.apache.felix.dm.annotation.api.ResourceAdapterService;
 import org.apache.felix.dm.annotation.api.ResourceDependency;
 import org.apache.felix.dm.annotation.api.ServiceDependency;
 import org.apache.felix.dm.annotation.api.Start;
-import org.apache.felix.dm.annotation.api.Registered;
 import org.apache.felix.dm.annotation.api.Stop;
 import org.apache.felix.dm.annotation.api.Unregistered;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
 import org.osgi.framework.Bundle;
 
 import aQute.bnd.osgi.Annotation;
 import aQute.bnd.osgi.ClassDataCollector;
 import aQute.bnd.osgi.Clazz;
-import aQute.bnd.osgi.Verifier;
 import aQute.bnd.osgi.Descriptors.TypeRef;
+import aQute.bnd.osgi.Verifier;
 
 /**
  * This is the scanner which does all the annotation parsing on a given class.
@@ -932,45 +933,255 @@
 
     /**
      * Parses a Property annotation (which represents a list of key-value pair).
+     * The properties are encoded using the following json form:
+     * 
+     * properties       ::= key-value-pair*
+     * key-value-pair   ::= key value
+     * value            ::= String | String[] | value-type
+     * value-type       ::= jsonObject with value-type-info
+     * value-type-info  ::= "type"=primitive java type
+     *                      "value"=String|String[]
+     *                      
+     * Exemple:
+     * 
+     *  "properties" : {
+     *      "string-param" : "string-value",
+     *      "string-array-param" : ["str1", "str2],
+     *      "long-param" : {"type":"java.lang.Long", "value":"1"}}
+     *      "long-array-param" : {"type":"java.lang.Long", "value":["1"]}}
+     *  }
+     * }
+     * 
      * @param annotation the annotation where the Param annotation is defined
      * @param attribute the attribute name which is of Param type
      * @param writer the object where the parsed attributes are written
      */
     private void parseProperties(Annotation annotation, EntryParam attribute, EntryWriter writer)
     {
-        Object[] parameters = annotation.get(attribute.toString());
-        Map<String, Object> properties = new HashMap<String, Object>();
-        if (parameters != null)
+        try
         {
-            for (Object p: parameters)
+            Object[] parameters = annotation.get(attribute.toString());
+            if (parameters != null)
             {
-                Annotation a = (Annotation) p;
-                String name = (String) a.get("name");
-                String value = (String) a.get("value");
-                if (value != null)
+                JSONObject properties = new JSONObject();
+                for (Object p : parameters)
                 {
-                    properties.put(name, value);
-                }
-                else
-                {
-                    Object[] values = a.get("values");
-                    if (values != null)
+                    Annotation a = (Annotation) p;
+                    String name = (String) a.get("name");
+
+                    String type = a.get("type");
+                    Class<?> classType;
+                    try
                     {
-                        // the values is an Object array of actual strings, and we must convert it into a String array.
-                        properties.put(name, Arrays.asList(values).toArray(new String[values.length]));
+                        classType = (type == null) ? String.class : Class.forName(Patterns.parseClass(type,
+                            Patterns.CLASS, 1));
+                    }
+                    catch (ClassNotFoundException e)
+                    {
+                        // Theorically impossible
+                        throw new IllegalArgumentException("Invalid Property type " + type
+                            + " from annotation " + annotation + " in class " + m_className);
+                    }
+
+                    Object[] values;
+
+                    if ((values = a.get("value")) != null)
+                    {
+                        values = checkPropertyType(name, classType, values);
+                        addProperty(properties, name, values, classType);
+                    }
+                    else if ((values = a.get("values")) != null)
+                    { // deprecated
+                        values = checkPropertyType(name, classType, values);
+                        addProperty(properties, name, values, classType);
+                    }
+                    else if ((values = a.get("longValue")) != null)
+                    {
+                        addProperty(properties, name, values, Long.class);
+                    }
+                    else if ((values = a.get("doubleValue")) != null)
+                    {
+                        addProperty(properties, name, values, Double.class);
+                    }
+                    else if ((values = a.get("floatValue")) != null)
+                    {
+                        addProperty(properties, name, values, Float.class);
+                    }
+                    else if ((values = a.get("intValue")) != null)
+                    {
+                        addProperty(properties, name, values, Integer.class);
+                    }
+                    else if ((values = a.get("byteValue")) != null)
+                    {
+                        addProperty(properties, name, values, Byte.class);
+                    }
+                    else if ((values = a.get("charValue")) != null)
+                    {
+                        addProperty(properties, name, values, Character.class);
+                    }
+                    else if ((values = a.get("booleanValue")) != null)
+                    {
+                        addProperty(properties, name, values, Boolean.class);
+                    }
+                    else if ((values = a.get("shortValue")) != null)
+                    {
+                        addProperty(properties, name, values, Short.class);
                     }
                     else
                     {
-                        throw new IllegalArgumentException("Invalid Property attribyte \"" + attribute
-                            + " from annotation " + annotation + " in class " + m_className);
+                        throw new IllegalArgumentException("Missing Property value from annotation "
+                            + annotation + " in class " + m_className);
                     }
                 }
+                writer.putJsonObject(attribute, properties);
             }
-            writer.putProperties(attribute, properties);
+        }
+        catch (JSONException e)
+        {
+            throw new IllegalArgumentException("UNexpected exception while parsing Property from annotation "
+                + annotation + " in class " + m_className, e);
         }
     }
     
     /**
+     * Checks if a property contains values that are compatible with a give primitive type.
+     * 
+     * @param name the property name
+     * @param values the values for the property name
+     * @param type the primitive type.
+     * @return the same property values, possibly modified if the type is 'Character' (the strings are converted to their character integer values). 
+     */
+    private Object[] checkPropertyType(String name, Class<?> type, Object ... values)
+    {
+        if (type.equals(String.class))
+        {
+            return values;
+        } else if (type.equals(Long.class)) {
+            for (Object value : values) {
+                try {
+                    Long.valueOf(value.toString());
+                } catch (NumberFormatException e) {
+                    throw new IllegalArgumentException("Property \"" + name + "\" in class " + m_className
+                        + " does not contain a valid Long value (" + value.toString() + ")");
+                }
+            }
+        } else if (type.equals(Double.class)) {
+            for (Object value : values) {
+                try {
+                    Double.valueOf(value.toString());
+                } catch (NumberFormatException e) {
+                    throw new IllegalArgumentException("Property \"" + name + "\" in class " + m_className
+                        + " does not contain a valid Double value (" + value.toString() + ")");
+                }
+            }
+        } else if (type.equals(Float.class)) {
+            for (Object value : values) {
+                try {
+                    Float.valueOf(value.toString());
+                } catch (NumberFormatException e) {
+                    throw new IllegalArgumentException("Property \"" + name + "\" in class " + m_className
+                        + " does not contain a valid Float value (" + value.toString() + ")");
+                }
+            }
+        } else if (type.equals(Integer.class)) {
+            for (Object value : values) {
+                try {
+                    Integer.valueOf(value.toString());
+                } catch (NumberFormatException e) {
+                    throw new IllegalArgumentException("Property \"" + name + "\" in class " + m_className
+                        + " does not contain a valid Integer value (" + value.toString() + ")");
+                }
+            }
+        } else if (type.equals(Byte.class)) {
+            for (Object value : values) {
+                try {
+                    Byte.valueOf(value.toString());
+                } catch (NumberFormatException e) {
+                    throw new IllegalArgumentException("Property \"" + name + "\" in class " + m_className
+                        + " does not contain a valid Byte value (" + value.toString() + ")");
+                }
+            }
+        } else if (type.equals(Character.class)) {
+            for (int i = 0; i < values.length; i++)
+            {
+                try
+                {
+                    // If the string is already an integer, don't modify it
+                    Integer.valueOf(values[i].toString());
+                }
+                catch (NumberFormatException e)
+                {
+                    // Converter the character string to its corresponding integer code.
+                    if (values[i].toString().length() != 1)
+                    {
+                        throw new IllegalArgumentException("Property \"" + name + "\" in class "
+                            + m_className + " does not contain a valid Character value (" + values[i] + ")");
+                    }
+                    try
+                    {
+                        values[i] = Integer.valueOf(values[i].toString().charAt(0));
+                    }
+                    catch (NumberFormatException e2)
+                    {
+                        throw new IllegalArgumentException("Property \"" + name + "\" in class "
+                            + m_className + " does not contain a valid Character value (" + values[i].toString()
+                            + ")");
+                    }
+                }
+            }
+        } else if (type.equals(Boolean.class)) {
+            for (Object value : values) {
+                if (! value.toString().equalsIgnoreCase("false") && ! value.toString().equalsIgnoreCase("true")) {
+                    throw new IllegalArgumentException("Property \"" + name + "\" in class " + m_className
+                        + " does not contain a valid Boolean value (" + value.toString() + ")");
+                }
+            }
+        } else if (type.equals(Short.class)) {
+            for (Object value : values) {
+                try {
+                    Short.valueOf(value.toString());
+                } catch (NumberFormatException e) {
+                    throw new IllegalArgumentException("Property \"" + name + "\" in class " + m_className
+                        + " does not contain a valid Short value (" + value.toString() + ")");
+                }
+            }
+        }
+
+        return values;
+    }
+
+    /**
+     * Adds a key/value(s) pair in a properties map
+     * @param properties the target properties map
+     * @param name the property name
+     * @param value the property value(s)
+     * @param type the property type
+     * @throws JSONException 
+     */
+    private void addProperty(JSONObject props, String name, Object value, Class type) throws JSONException {
+        if (value.getClass().isArray())
+        {
+            Object[] array = (Object[]) value;
+            if (array.length == 1)
+            {
+                value = array[0];
+            }
+        }
+
+        if (type.equals(String.class))
+        {
+            props.put(name, value.getClass().isArray() ? new JSONArray(value) : value);
+        }
+        else
+        {
+            JSONObject jsonValue = new JSONObject();
+            jsonValue.put("type", type.getName());
+            jsonValue.put("value", value.getClass().isArray() ? new JSONArray(value) : value);
+            props.put(name, jsonValue);
+        }
+    }
+
+    /**
      * Parse Inject annotation, used to inject some special classes in some fields
      * (BundleContext/DependencyManager etc ...)
      * @param annotation the Inject annotation
diff --git a/dependencymanager/annotation/src/main/java/org/apache/felix/dm/annotation/plugin/bnd/EntryWriter.java b/dependencymanager/annotation/src/main/java/org/apache/felix/dm/annotation/plugin/bnd/EntryWriter.java
index b59f920..8731dd8 100644
--- a/dependencymanager/annotation/src/main/java/org/apache/felix/dm/annotation/plugin/bnd/EntryWriter.java
+++ b/dependencymanager/annotation/src/main/java/org/apache/felix/dm/annotation/plugin/bnd/EntryWriter.java
@@ -113,39 +113,12 @@
     }
 
     /**
-     * Put a Map parameter in the descriptor entry. The map values must be either Strings or Strings arrays.
+     * Puts a json object.
+     * @throws JSONException 
      */
-    public void putProperties(EntryParam param, Map<String, Object> properties)
+    public void putJsonObject(EntryParam param, JSONObject jsonObject) throws JSONException
     {
-        checkType(param.toString());
-
-        try
-        {
-            JSONObject props = new JSONObject();
-            for (String key: properties.keySet())
-            {
-                Object value = properties.get(key);
-                if (value instanceof String)
-                {
-                    props.put(key, value);
-                }
-                else if (value instanceof String[])
-                {
-                    props.put(key, new JSONArray(Arrays.asList((String[]) value)));
-                }
-                else
-                {
-                    throw new IllegalArgumentException("invalid property value: " + value);
-                }
-            }
-            m_json.put(param.toString(), props);
-        }
-
-        catch (JSONException e)
-        {
-            throw new IllegalArgumentException("invalid properties for " + param + ": "
-                + properties, e);
-        }
+        m_json.put(param.toString(),  jsonObject);
     }
 
     /**