Fixed FELIX-3758, FELIX-3757 & FELIX-3756:

- validation of required attributes without min, max or optionValues was not performed;
- validation of attributes with cardinality != 0 was not performed correctly;
- added a validation for values of invalid types to ensure they are not accepted as-is.



git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@1409877 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/metatype/src/main/java/org/apache/felix/metatype/AD.java b/metatype/src/main/java/org/apache/felix/metatype/AD.java
index 8899ccf..a083f68 100644
--- a/metatype/src/main/java/org/apache/felix/metatype/AD.java
+++ b/metatype/src/main/java/org/apache/felix/metatype/AD.java
@@ -47,6 +47,12 @@
 
     /**
      * The message returned from the {@link #validate(String)} method if the
+     * value is invalid considering its type (value is "%invalid value").
+     */
+    public static final String VALIDATE_INVALID_VALUE = "%invalid value";
+
+    /**
+     * The message returned from the {@link #validate(String)} method if the
      * value is greater than the specified {@link #getMax() maximum value}
      * (value is "%greater than maximum").
      */
@@ -175,109 +181,7 @@
      */
     public String validate( String valueString )
     {
-        // no validation if no min and max
-        if ( getMin() == null && getMax() == null && getOptionValues() == null )
-        {
-            return null;
-        }
-
-        // min/max for strings and passwords indicates the length
-        final Comparable value;
-        if ( getType() == AttributeDefinition.STRING || getType() == AttributeDefinition.PASSWORD )
-        {
-            if ( valueString == null )
-            {
-                if ( isRequired() )
-                {
-                    return VALIDATE_MISSING;
-                }
-
-                return ""; // accept null value
-            }
-
-            if ( getMin() != null )
-            {
-                try
-                {
-                    if ( valueString.length() < Integer.parseInt( getMin() ) )
-                    {
-                        return VALIDATE_LESS_THAN_MINIMUM;
-                    }
-                }
-                catch ( NumberFormatException nfe )
-                {
-                    // cannot check min length
-                }
-            }
-
-            if ( getMax() != null )
-            {
-                try
-                {
-                    if ( valueString.length() > Integer.parseInt( getMax() ) )
-                    {
-                        return VALIDATE_GREATER_THAN_MAXIMUM;
-                    }
-                }
-                catch ( NumberFormatException nfe )
-                {
-                    // cannot check min length
-                }
-            }
-
-            value = valueString;
-        }
-        else
-        {
-            value = convertToType( valueString );
-            if ( value == null )
-            {
-                if ( isRequired() )
-                {
-                    return VALIDATE_MISSING;
-                }
-
-                return ""; // accept null value
-            }
-
-            Comparable other = convertToType( getMin() );
-            if ( other != null )
-            {
-                if ( value.compareTo( other ) < 0 )
-                {
-                    return VALIDATE_LESS_THAN_MINIMUM;
-                }
-            }
-
-            other = convertToType( getMax() );
-            if ( other != null )
-            {
-                if ( value.compareTo( other ) > 0 )
-                {
-                    return VALIDATE_GREATER_THAN_MAXIMUM;
-                }
-            }
-        }
-
-        String[] optionValues = getOptionValues();
-        if ( optionValues != null && optionValues.length > 0 )
-        {
-            for ( int i = 0; i < optionValues.length; i++ )
-            {
-                Comparable other = convertToType( optionValues[i] );
-                if ( value.compareTo( other ) == 0 )
-                {
-                    // one of the option values
-                    return "";
-                }
-            }
-
-            // not any of the option values, fail
-            return VALIDATE_NOT_A_VALID_OPTION;
-        }
-
-        // finally, we accept the value
-        return "";
+    	return ADValidator.validate(this, valueString);
     }
 
 
@@ -440,59 +344,50 @@
 
     public static String[] splitList( String listString )
     {
-        // check for non-existing or empty lists
-        if ( listString == null )
-        {
-            return null;
-        }
-        else if ( listString.length() == 0 )
-        {
-            return new String[]
-                { "" };
-        }
+		if (listString == null) {
+			return null;
+		} else if (listString.length() == 0) {
+			return new String[] { "" };
+		}
 
-        List values = new ArrayList();
-        boolean escape = false;
-        StringBuffer buf = new StringBuffer();
-        for ( int i = 0; i < listString.length(); i++ )
-        {
-            char c = listString.charAt( i );
+		List strings = new ArrayList();
+		StringBuffer sb = new StringBuffer();
 
-            if ( escape )
-            {
-                // just go ahead
-                escape = false;
-            }
-            else if ( c == ',' )
-            {
-                String value = buf.toString().trim();
-                if ( value.length() > 0 )
-                {
-                    values.add( value );
-                }
-                buf.delete( 0, buf.length() );
-                continue;
-            }
-            else if ( c == '\\' )
-            {
-                escape = true;
-                continue;
-            }
+		int length = listString.length();
+		boolean escaped = false;
+		
+		for (int i = 0; i < length; i++) {
+			char ch = listString.charAt(i);
+			if (ch == '\\') {
+				if (!escaped) {
+					escaped = true;
+					continue;
+				}
+			} else if (ch == ',') {
+				if (!escaped) {
+					// unescaped comma, this is a string delimiter...
+					strings.add(sb.toString());
+					sb.setLength(0);
+					continue;
+				}
+			} else if (ch == ' ') {
+				// we should ignore spaces normally, unless they are escaped...
+				if (!escaped) {
+					continue;
+				}
+			} else if (Character.isWhitespace(ch)) {
+				// Other whitespaces are ignored...
+				continue;
+			}
 
-            buf.append( c );
-        }
+			sb.append(ch);
+			escaped = false;
+		}
 
-        // add last string
-        if ( buf.length() > 0 )
-        {
-            String value = buf.toString().trim();
-            if ( value.length() > 0 )
-            {
-                values.add( value );
-            }
-        }
+		// Always add the last string, as it contains everything after the last comma...
+		strings.add(sb.toString());
 
-        return values.isEmpty() ? null : ( String[] ) values.toArray( new String[values.size()] );
+		return (String[]) strings.toArray(new String[strings.size()]);
     }
 
 
diff --git a/metatype/src/main/java/org/apache/felix/metatype/ADValidator.java b/metatype/src/main/java/org/apache/felix/metatype/ADValidator.java
new file mode 100644
index 0000000..d82b136
--- /dev/null
+++ b/metatype/src/main/java/org/apache/felix/metatype/ADValidator.java
@@ -0,0 +1,348 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.felix.metatype;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+import org.osgi.service.metatype.AttributeDefinition;
+
+/**
+ * Provides various validation routines used by the {@link AD#validate(String)}
+ * method.
+ * 
+ * @author <a href="mailto:dev@felix.apache.org">Felix Project Team</a>
+ */
+final class ADValidator {
+
+	/**
+	 * Validates a given input string according to the type specified by the
+	 * given attribute definition.
+	 * 
+	 * @param ad
+	 *            the attribute definition to use in the validation;
+	 * @param rawInput
+	 *            the raw input value to validate.
+	 * @return <code>null</code> if no validation is available, <tt>""</tt> if
+	 *         validation was successful, or any other non-empty string in case
+	 *         validation fails.
+	 */
+	public static String validate(AD ad, String rawInput) {
+		// Handle the case in which the given input is not defined...
+		if (rawInput == null) {
+			if (ad.isRequired()) {
+				return AD.VALIDATE_MISSING;
+			}
+
+			return ""; // accept null value...
+		}
+
+		// Raw input is defined, validate it further
+		String[] input;
+
+		if (ad.getCardinality() == 0) {
+			input = new String[] { rawInput.trim() };
+		} else {
+			input = AD.splitList(rawInput);
+		}
+
+		int type = ad.getType();
+		switch (type) {
+		case AttributeDefinition.BOOLEAN:
+			return validateBooleanValue(ad, input);
+
+		case AttributeDefinition.CHARACTER:
+			return validateCharacterValue(ad, input);
+
+		case AttributeDefinition.BIGDECIMAL:
+		case AttributeDefinition.BIGINTEGER:
+		case AttributeDefinition.BYTE:
+		case AttributeDefinition.DOUBLE:
+		case AttributeDefinition.FLOAT:
+		case AttributeDefinition.INTEGER:
+		case AttributeDefinition.LONG:
+		case AttributeDefinition.SHORT:
+			return validateNumericValue(ad, input);
+
+		case AttributeDefinition.PASSWORD:
+		case AttributeDefinition.STRING:
+			return validateString(ad, input);
+
+		default:
+			return null; // no validation present...
+		}
+	}
+
+	/**
+	 * Searches for a given search value in a given array of options.
+	 * 
+	 * @param searchValue
+	 *            the value to search for;
+	 * @param optionValues
+	 *            the values to search in.
+	 * @return <code>null</code> if the given search value is not found in the
+	 *         given options, the searched value if found, or <tt>""</tt> if no
+	 *         search value or options were given.
+	 */
+	private static String findOptionValue(String searchValue, String[] optionValues) {
+		if ((searchValue == null) || (optionValues == null) || (optionValues.length == 0)) {
+			// indicates that we've not searched
+			return "";
+		}
+
+		for (int i = 0; i < optionValues.length; i++) {
+			if (optionValues[i].equals(searchValue)) {
+				return optionValues[i];
+			}
+		}
+
+		return null;
+	}
+
+	/**
+	 * Parses a given string value into a numeric type.
+	 * 
+	 * @param type
+	 *            the type to parse;
+	 * @param value
+	 *            the value to parse.
+	 * @return a {@link Number} representation of the given value, or
+	 *         <code>null</code> if the input was <code>null</code>, empty, or
+	 *         not a numeric type.
+	 * @throws NumberFormatException
+	 *             in case the given value cannot be parsed as numeric value.
+	 */
+	private static Comparable parseNumber(int type, String value) throws NumberFormatException {
+		if ((value != null) && (value.length() > 0)) {
+			switch (type) {
+			case AttributeDefinition.BIGDECIMAL:
+				return new BigDecimal(value);
+			case AttributeDefinition.BIGINTEGER:
+				return new BigInteger(value);
+			case AttributeDefinition.BYTE:
+				return Byte.valueOf(value);
+			case AttributeDefinition.SHORT:
+				return Short.valueOf(value);
+			case AttributeDefinition.INTEGER:
+				return Integer.valueOf(value);
+			case AttributeDefinition.LONG:
+				return Long.valueOf(value);
+			case AttributeDefinition.FLOAT:
+				return Float.valueOf(value);
+			case AttributeDefinition.DOUBLE:
+				return Double.valueOf(value);
+			default:
+				return null;
+			}
+		}
+		return null;
+	}
+
+	/**
+	 * Parses a given string value as character, allowing <code>null</code>
+	 * -values and empty values to be given as input.
+	 * 
+	 * @param value
+	 *            the value to parse as character, can be <code>null</code> or
+	 *            an empty value.
+	 * @return the character value if, and only if, the given input was non-
+	 *         <code>null</code> and a non-empty string.
+	 */
+	private static Character parseOptionalChar(String value) {
+		if ((value != null) && (value.length() > 0)) {
+			return Character.valueOf(value.charAt(0));
+		}
+		return null;
+	}
+
+	/**
+	 * Parses a given string value as integer, allowing <code>null</code>-values
+	 * and invalid numeric values to be given as input.
+	 * 
+	 * @param value
+	 *            the value to parse as integer, can be <code>null</code> or a
+	 *            non-numeric value.
+	 * @return the integer value if, and only if, the given input was non-
+	 *         <code>null</code> and a valid integer representation.
+	 */
+	private static Integer parseOptionalInt(String value) {
+		if (value != null) {
+			try {
+				return Integer.valueOf(value);
+			} catch (NumberFormatException e) {
+				// Ignore; invalid value...
+			}
+		}
+		return null;
+	}
+
+	/**
+	 * Validates a given input string as boolean value.
+	 * 
+	 * @param ad
+	 *            the attribute definition to use in the validation;
+	 * @param input
+	 *            the array with input values to validate.
+	 * @return <code>null</code> if no validation is available, <tt>""</tt> if
+	 *         validation was successful, or any other non-empty string in case
+	 *         validation fails.
+	 */
+	private static String validateBooleanValue(AD ad, String[] input) {
+		for (int i = 0; i < input.length; i++) {
+			int length = input[i].length();
+			if ((length == 0) && ad.isRequired()) {
+				return AD.VALIDATE_MISSING;
+			} else if (length > 0 && !"true".equalsIgnoreCase(input[i]) && !"false".equalsIgnoreCase(input[i])) {
+				return AD.VALIDATE_INVALID_VALUE;
+			}
+		}
+
+		String[] optionValues = ad.getOptionValues();
+		if (optionValues != null && optionValues.length > 0) {
+			return null; // no validation possible for this type...
+		}
+
+		return ""; // accept given value...
+	}
+
+	/**
+	 * Validates a given input string as character value.
+	 * 
+	 * @param ad
+	 *            the attribute definition to use in the validation;
+	 * @param input
+	 *            the array with input values to validate.
+	 * @return <code>null</code> if no validation is available, <tt>""</tt> if
+	 *         validation was successful, or any other non-empty string in case
+	 *         validation fails.
+	 */
+	private static String validateCharacterValue(AD ad, String[] input) {
+		Character min = parseOptionalChar(ad.getMin());
+		Character max = parseOptionalChar(ad.getMax());
+		String[] optionValues = ad.getOptionValues();
+
+		for (int i = 0; i < input.length; i++) {
+			Character ch = null;
+			int length = input[i].length();
+			if (length > 1) {
+				return AD.VALIDATE_GREATER_THAN_MAXIMUM;
+			} else if ((length == 0) && ad.isRequired()) {
+				return AD.VALIDATE_MISSING;
+			} else if (length == 1) {
+				ch = Character.valueOf(input[i].charAt(0));
+				// Check whether the minimum value is adhered for all values...
+				if ((min != null) && (ch.compareTo(min) < 0)) {
+					return AD.VALIDATE_LESS_THAN_MINIMUM;
+				}
+				// Check whether the maximum value is adhered for all values...
+				if ((max != null) && (ch.compareTo(max) > 0)) {
+					return AD.VALIDATE_GREATER_THAN_MAXIMUM;
+				}
+			}
+
+			if (findOptionValue(input[i], optionValues) == null) {
+				return AD.VALIDATE_NOT_A_VALID_OPTION;
+			}
+		}
+
+		return ""; // accept given value...
+	}
+
+	/**
+	 * Validates a given input string as numeric value.
+	 * 
+	 * @param ad
+	 *            the attribute definition to use in the validation;
+	 * @param input
+	 *            the array with input values to validate.
+	 * @return <code>null</code> if no validation is available, <tt>""</tt> if
+	 *         validation was successful, or any other non-empty string in case
+	 *         validation fails.
+	 */
+	private static String validateNumericValue(AD ad, String[] input) {
+		Integer min = parseOptionalInt(ad.getMin());
+		Integer max = parseOptionalInt(ad.getMax());
+		String[] optionValues = ad.getOptionValues();
+
+		for (int i = 0; i < input.length; i++) {
+			Comparable value = null;
+			try {
+				value = parseNumber(ad.getType(), input[i]);
+			} catch (NumberFormatException e) {
+				return AD.VALIDATE_INVALID_VALUE;
+			}
+
+			if ((value == null) && ad.isRequired()) {
+				// Possible if the cardinality != 0 and input was something like
+				// "0,,1"...
+				return AD.VALIDATE_MISSING;
+			}
+			// Check whether the minimum value is adhered for all values...
+			if ((min != null) && (value != null) && (value.compareTo(min) < 0)) {
+				return AD.VALIDATE_LESS_THAN_MINIMUM;
+			}
+			// Check whether the maximum value is adhered for all values...
+			if ((max != null) && (value != null) && (value.compareTo(max) > 0)) {
+				return AD.VALIDATE_GREATER_THAN_MAXIMUM;
+			}
+
+			if (findOptionValue(input[i], optionValues) == null) {
+				return AD.VALIDATE_NOT_A_VALID_OPTION;
+			}
+		}
+
+		return ""; // accept given value...
+	}
+
+	/**
+	 * Validates a given input string as string (or password).
+	 * 
+	 * @param ad
+	 *            the attribute definition to use in the validation;
+	 * @param input
+	 *            the array with input values to validate.
+	 * @return <code>null</code> if no validation is available, <tt>""</tt> if
+	 *         validation was successful, or any other non-empty string in case
+	 *         validation fails.
+	 */
+	private static String validateString(AD ad, String[] input) {
+		Integer min = parseOptionalInt(ad.getMin());
+		Integer max = parseOptionalInt(ad.getMax());
+		String[] optionValues = ad.getOptionValues();
+
+		for (int i = 0; i < input.length; i++) {
+			int length = input[i].length();
+			// Check whether the minimum length is adhered for all values...
+			if ((min != null) && (length < min.intValue())) {
+				return AD.VALIDATE_LESS_THAN_MINIMUM;
+			}
+			// Check whether the maximum length is adhered for all values...
+			if ((max != null) && (length > max.intValue())) {
+				return AD.VALIDATE_GREATER_THAN_MAXIMUM;
+			}
+
+			if (findOptionValue(input[i], optionValues) == null) {
+				return AD.VALIDATE_NOT_A_VALID_OPTION;
+			}
+		}
+
+		return ""; // accept given value...
+	}
+}
diff --git a/metatype/src/test/java/org/apache/felix/metatype/ADTest.java b/metatype/src/test/java/org/apache/felix/metatype/ADTest.java
index 5c4980b..065f0c9 100644
--- a/metatype/src/test/java/org/apache/felix/metatype/ADTest.java
+++ b/metatype/src/test/java/org/apache/felix/metatype/ADTest.java
@@ -78,6 +78,31 @@
     }
 
 
+    public void testEmptySecond()
+    {
+        String value0 = "value0";
+        String value1 = "";
+        String listString = value0 + ",";
+        String[] list = AD.splitList( listString );
+        assertNotNull( list );
+        assertEquals( 2, list.length );
+        assertEquals( value0, list[0] );
+        assertEquals( value1, list[1] );
+    }
+
+    public void testSpacedSecond()
+    {
+        String value0 = "value0";
+        String value1 = "";
+        String listString = value0 + ", ";
+        String[] list = AD.splitList( listString );
+        assertNotNull( list );
+        assertEquals( 2, list.length );
+        assertEquals( value0, list[0] );
+        assertEquals( value1, list[1] );
+    }
+
+
     public void testSingleBlanks()
     {
         String value0 = "value";
@@ -106,9 +131,9 @@
     {
         String value0 = "a,b";
         String value1 = "b,c";
-        String value2 = "c\\";
+        String value2 = " c\\";
         String value3 = "d";
-        String listString = "a\\,b,b\\,c, c\\\\,d";
+        String listString = "a\\,b,b\\,c,\\ c\\\\,d";
         String[] list = AD.splitList( listString );
         assertNotNull( list );
         assertEquals( 4, list.length );
@@ -133,4 +158,42 @@
         assertEquals( AttributeDefinition.PASSWORD, AD.toType( "Password" ) );
         assertEquals( AttributeDefinition.STRING, AD.toType( "JohnDoe" ) );
     }
+    
+    /**
+     * FELIX-3757: if an AD has only its 'required' property set, but no 
+     * min/max or option values defined, the validation still should detect 
+     * empty values. 
+     */
+    public void testValidateRequiredValueWithMinimalOptions() {
+    	AD ad = new AD();
+    	ad.setType("Integer");
+    	ad.setRequired(true);
+
+    	assertEquals(AD.VALIDATE_MISSING, ad.validate(null));
+    }
+    
+    /**
+     * FELIX-3756: if an AD is optional, but its validate method is called
+     * with invalid data, the value is regarded missing.
+     */
+    public void testValidateOptionalValueWithInvalidData() {
+    	AD ad = new AD();
+    	ad.setType("Integer");
+    	ad.setRequired(false);
+    	
+    	assertEquals(AD.VALIDATE_INVALID_VALUE, ad.validate("abc"));
+    }
+    
+    /**
+     * FELIX-3758: if an AD has a cardinality != 0, the validation method
+     * cannot handle a comma-separated input.
+     */
+    public void testValidateValueWithMultiValueCardinality() {
+    	AD ad = new AD();
+    	ad.setType("Integer");
+    	ad.setCardinality(2);
+    	ad.setRequired(true);
+    	
+    	assertEquals("", ad.validate("1,2"));
+    }
 }
diff --git a/metatype/src/test/java/org/apache/felix/metatype/ADValidatorTest.java b/metatype/src/test/java/org/apache/felix/metatype/ADValidatorTest.java
new file mode 100644
index 0000000..8640629
--- /dev/null
+++ b/metatype/src/test/java/org/apache/felix/metatype/ADValidatorTest.java
@@ -0,0 +1,269 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.felix.metatype;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import junit.framework.TestCase;
+
+/**
+ * Test cases for {@link ADValidator}.
+ * 
+ * @author <a href="mailto:dev@felix.apache.org">Felix Project Team</a>
+ */
+public class ADValidatorTest extends TestCase {
+
+	/**
+	 * Tests the validation of boolean is only done minimally.
+	 */
+	public void testValidateBoolean() {
+		AD ad = new AD();
+		ad.setType("Boolean");
+		ad.setRequired(false);
+
+		// optional value
+		assertEquals("", ADValidator.validate(ad, null));
+		// adhere minimal value
+		assertEquals("", ADValidator.validate(ad, "true"));
+		// adhere maximal value
+		assertEquals("", ADValidator.validate(ad, "false"));
+		// not a valid value
+		assertEquals(AD.VALIDATE_INVALID_VALUE, ADValidator.validate(ad, "foobar"));
+
+		ad.setCardinality(3); // up to three values are allowed...
+		
+		// mandatory value
+		assertEquals("", ADValidator.validate(ad, null));
+		// 2nd value is missing
+		assertEquals("", ADValidator.validate(ad, "true,,false"));
+
+		ad.setRequired(true);
+
+		// mandatory value
+		assertEquals(AD.VALIDATE_MISSING, ADValidator.validate(ad, null));
+		// 2nd value is missing
+		assertEquals(AD.VALIDATE_MISSING, ADValidator.validate(ad, "false,,true"));
+		assertEquals("", ADValidator.validate(ad, "false, true, false"));
+
+		ad.setOptions(Collections.singletonMap("true", "Yes!"));
+
+		assertEquals(null, ADValidator.validate(ad, "false, true, false"));
+	}
+
+	/**
+	 * Tests the validation of characters with only limited set of options.
+	 */
+	public void testValidateCharacterOptionValues() {
+		AD ad = new AD();
+		ad.setType("Char");
+		ad.setRequired(false);
+
+		// optional value
+		assertEquals("", ADValidator.validate(ad, null));
+		// option too long
+		assertEquals(AD.VALIDATE_GREATER_THAN_MAXIMUM, ADValidator.validate(ad, "ab"));
+		// adhere first option value
+		assertEquals("", ADValidator.validate(ad, "b"));
+		// adhere last option value
+		assertEquals("", ADValidator.validate(ad, "e"));
+
+		ad.setCardinality(3); // up to three values are allowed...
+
+		// mandatory value
+		assertEquals("", ADValidator.validate(ad, ""));
+		// 2nd value is missing
+		assertEquals("", ADValidator.validate(ad, "b,,c"));
+
+		ad.setRequired(true);
+
+		// mandatory value
+		assertEquals(AD.VALIDATE_MISSING, ADValidator.validate(ad, null));
+		// 2nd value is missing
+		assertEquals(AD.VALIDATE_MISSING, ADValidator.validate(ad, "b,,c"));
+		// adhere minimal values
+		assertEquals("", ADValidator.validate(ad, "b, c, d"));
+		// adhere maximal values
+		assertEquals("", ADValidator.validate(ad, "c, d, e"));
+
+		Map options = new HashMap();
+		options.put("b", "B");
+		options.put("c", "C");
+		options.put("d", "D");
+		options.put("e", "E");
+
+		ad.setOptions(options);
+		// no option given
+		assertEquals(AD.VALIDATE_MISSING, ADValidator.validate(ad, ""));
+		// invalid option
+		assertEquals(AD.VALIDATE_NOT_A_VALID_OPTION, ADValidator.validate(ad, "a"));
+		// too great
+		assertEquals(AD.VALIDATE_NOT_A_VALID_OPTION, ADValidator.validate(ad, "f"));
+		// 2nd value is too less
+		assertEquals(AD.VALIDATE_NOT_A_VALID_OPTION, ADValidator.validate(ad, "b,a,c"));
+		// 3rd value is too great
+		assertEquals(AD.VALIDATE_NOT_A_VALID_OPTION, ADValidator.validate(ad, "d, e, f"));
+
+		ad.setMin("b");
+		ad.setMax("c");
+		ad.setOptions(Collections.emptyMap());
+
+		// adhere minimal values
+		assertEquals("", ADValidator.validate(ad, "b, c, b"));
+		// d is too great
+		assertEquals(AD.VALIDATE_GREATER_THAN_MAXIMUM, ADValidator.validate(ad, "b, c, d"));
+		// a is too less
+		assertEquals(AD.VALIDATE_LESS_THAN_MINIMUM, ADValidator.validate(ad, "a, b, c"));
+	}
+
+	/**
+	 * Tests the validation of characters with only limited set of options.
+	 */
+	public void testValidateDoubleOptionValues() {
+		Map options = new HashMap();
+		options.put("1.1", "B");
+		options.put("2.2", "C");
+		options.put("3.3", "D");
+		options.put("4.4", "E");
+
+		AD ad = new AD();
+		ad.setType("Double");
+		ad.setOptions(options);
+		ad.setRequired(false);
+
+		// optional value
+		assertEquals("", ADValidator.validate(ad, null));
+		// invalid option
+		assertEquals(AD.VALIDATE_NOT_A_VALID_OPTION, ADValidator.validate(ad, "1.0"));
+		// adhere first option value
+		assertEquals("", ADValidator.validate(ad, "1.1"));
+		// adhere last option value
+		assertEquals("", ADValidator.validate(ad, "4.4"));
+		// too great
+		assertEquals(AD.VALIDATE_NOT_A_VALID_OPTION, ADValidator.validate(ad, "4.5"));
+
+		ad.setCardinality(3); // up to three values are allowed...
+		ad.setRequired(true);
+
+		// mandatory value
+		assertEquals(AD.VALIDATE_MISSING, ADValidator.validate(ad, null));
+		// 2nd value is too less
+		assertEquals(AD.VALIDATE_NOT_A_VALID_OPTION, ADValidator.validate(ad, "1.1,1.0,2.2"));
+		// adhere minimal values
+		assertEquals("", ADValidator.validate(ad, "1.1, 2.2, 3.3"));
+		// adhere maximal values
+		assertEquals("", ADValidator.validate(ad, "2.2, 3.3, 4.4"));
+		// 3rd value is too great
+		assertEquals(AD.VALIDATE_NOT_A_VALID_OPTION, ADValidator.validate(ad, "3.3, 4.4, 5.5"));
+	}
+
+	/**
+	 * Tests the validation of integers is based on the minimum and maximum values.
+	 */
+	public void testValidateInteger() {
+		AD ad = new AD();
+		ad.setType("Integer");
+		ad.setMin("3"); // only values greater than 2
+		ad.setMax("6"); // only values less than 7
+		ad.setRequired(false);
+
+		// optional value
+		assertEquals("", ADValidator.validate(ad, null));
+		// too less
+		assertEquals(AD.VALIDATE_LESS_THAN_MINIMUM, ADValidator.validate(ad, "2"));
+		// adhere minimal value
+		assertEquals("", ADValidator.validate(ad, "3"));
+		// adhere maximal value
+		assertEquals("", ADValidator.validate(ad, "6"));
+		// too great
+		assertEquals(AD.VALIDATE_GREATER_THAN_MAXIMUM, ADValidator.validate(ad, "7"));
+
+		ad.setCardinality(3); // up to three values are allowed...
+		
+		// mandatory value
+		assertEquals("", ADValidator.validate(ad, null));
+		// 2nd value is missing
+		assertEquals("", ADValidator.validate(ad, "3,,3"));
+
+		ad.setRequired(true);
+
+		// mandatory value
+		assertEquals(AD.VALIDATE_MISSING, ADValidator.validate(ad, null));
+		// 2nd value is missing
+		assertEquals(AD.VALIDATE_MISSING, ADValidator.validate(ad, "3,,3"));
+		// 2nd value is invalid
+		assertEquals(AD.VALIDATE_INVALID_VALUE, ADValidator.validate(ad, "3,a,3"));
+		// 2nd value is too less
+		assertEquals(AD.VALIDATE_LESS_THAN_MINIMUM, ADValidator.validate(ad, "3,2,3"));
+		// adhere minimal values
+		assertEquals("", ADValidator.validate(ad, "3, 4, 5"));
+		// adhere maximal values
+		assertEquals("", ADValidator.validate(ad, "6, 5, 4"));
+		// 3rd value is too great
+		assertEquals(AD.VALIDATE_GREATER_THAN_MAXIMUM, ADValidator.validate(ad, "5, 6, 7"));
+	}
+
+	/**
+	 * Tests the validation of strings is based on the minimum and maximum lengths.
+	 */
+	public void testValidateString() {
+		AD ad = new AD();
+		ad.setType("String");
+		ad.setRequired(false);
+
+		// optional value
+		assertEquals("", ADValidator.validate(ad, null));
+		// any length of input is accepted
+		assertEquals("", ADValidator.validate(ad, "1234567890"));
+
+		ad.setMin("3"); // minimal length == 3
+		ad.setMax("6"); // maximum length == 6
+
+		// too short
+		assertEquals(AD.VALIDATE_LESS_THAN_MINIMUM, ADValidator.validate(ad, "12"));
+		// adhere minimum length
+		assertEquals("", ADValidator.validate(ad, "123"));
+		// adhere maximum length
+		assertEquals("", ADValidator.validate(ad, "12356"));
+		// too long
+		assertEquals(AD.VALIDATE_GREATER_THAN_MAXIMUM, ADValidator.validate(ad, "1234567"));
+
+		ad.setCardinality(3); // up to three values are allowed...
+		ad.setRequired(true);
+
+		// mandatory value
+		assertEquals(AD.VALIDATE_MISSING, ADValidator.validate(ad, null));
+		// 2nd value is too short
+		assertEquals(AD.VALIDATE_LESS_THAN_MINIMUM, ADValidator.validate(ad, "321,12,123"));
+		// adhere minimum lengths
+		assertEquals("", ADValidator.validate(ad, "123, 123, 123"));
+		// adhere maximum lengths
+		assertEquals("", ADValidator.validate(ad, "12356, 654321, 123456"));
+		// 3rd value is too long
+		assertEquals(AD.VALIDATE_GREATER_THAN_MAXIMUM, ADValidator.validate(ad, "123, 123, 1234567"));
+		
+		ad.setOptions(Collections.singletonMap("123", "foo"));
+
+		// adhere minimum lengths
+		assertEquals("", ADValidator.validate(ad, "123, 123, 123"));
+		assertEquals(AD.VALIDATE_NOT_A_VALID_OPTION, ADValidator.validate(ad, "2134"));
+	}
+}