Coverage Report - org.jbehave.core.steps.ParameterConverters
 
Classes in this File Line Coverage Branch Coverage Complexity
ParameterConverters
100%
28/28
100%
6/6
2.474
ParameterConverters$DateConverter
100%
12/12
75%
3/4
2.474
ParameterConverters$ExamplesTableConverter
100%
9/9
100%
2/2
2.474
ParameterConverters$MethodReturningConverter
100%
10/10
100%
2/2
2.474
ParameterConverters$NumberConverter
100%
37/37
100%
36/36
2.474
ParameterConverters$NumberListConverter
100%
19/19
100%
8/8
2.474
ParameterConverters$ParameterConverter
N/A
N/A
2.474
ParameterConverters$ParameterConvertionFailed
100%
2/2
N/A
2.474
ParameterConverters$StringListConverter
100%
14/14
100%
8/8
2.474
 
 1  
 package org.jbehave.core.steps;
 2  
 
 3  
 import org.jbehave.core.model.ExamplesTable;
 4  
 import org.jbehave.core.model.ExamplesTableFactory;
 5  
 
 6  
 import java.lang.reflect.Method;
 7  
 import java.lang.reflect.ParameterizedType;
 8  
 import java.lang.reflect.Type;
 9  
 import java.math.BigDecimal;
 10  
 import java.math.BigInteger;
 11  
 import java.text.DateFormat;
 12  
 import java.text.NumberFormat;
 13  
 import java.text.ParseException;
 14  
 import java.text.SimpleDateFormat;
 15  
 import java.util.ArrayList;
 16  
 import java.util.Date;
 17  
 import java.util.List;
 18  
 import java.util.Locale;
 19  
 
 20  
 import static java.util.Arrays.asList;
 21  
 
 22  
 /**
 23  
  * <p>
 24  
  * Facade responsible for converting parameter values to Java objects.
 25  
  * </p>
 26  
  * <p>
 27  
  * Several converters are provided out-of-the-box:
 28  
  * <ul>
 29  
  * <li>{@link ParameterConverters.NumberConverter NumberConverter}</li>
 30  
  * <li>{@link ParameterConverters.NumberListConverter NumberListConverter}</li>
 31  
  * <li>{@link ParameterConverters.StringListConverter StringListConverter}</li>
 32  
  * <li>{@link ParameterConverters.DateConverter DateConverter}</li>
 33  
  * <li>{@link ParameterConverters.ExamplesTableConverter ExamplesTableConverter}</li>
 34  
  * <li>{@link ParameterConverters.MethodReturningConverter MethodReturningConverter}</li>
 35  
  * </ul>
 36  
  * </p>
 37  
  */
 38  
 public class ParameterConverters {
 39  
 
 40  1
     static final Locale DEFAULT_NUMBER_FORMAT_LOCAL = Locale.ENGLISH;
 41  
     static final String DEFAULT_COMMA = ",";
 42  
         
 43  
     private static final String NEWLINES_PATTERN = "(\n)|(\r\n)";
 44  1
     private static final String SYSTEM_NEWLINE = System.getProperty("line.separator");
 45  
     
 46  
     private final StepMonitor monitor;
 47  245
     private final List<ParameterConverter> converters = new ArrayList<ParameterConverter>();
 48  
 
 49  
     /** 
 50  
      * Default Parameters use a SilentStepMonitor, has English as Locale and use "," as list separator. 
 51  
      */
 52  
     public ParameterConverters() {
 53  245
         this(new SilentStepMonitor());
 54  245
     }
 55  
 
 56  
     /** 
 57  
      * Default ParameterConverters have English as Locale and use "," as list separator. 
 58  
      */
 59  
     public ParameterConverters(StepMonitor monitor) {
 60  245
             this(monitor , DEFAULT_NUMBER_FORMAT_LOCAL, DEFAULT_COMMA);
 61  245
     }
 62  
 
 63  
     /** 
 64  
      * Creates an ParameterConvertors for a given Locale.  When selecting
 65  
      * a listSeparator, please make sure that this character doesn't have a special
 66  
      * meaning in your Locale (for instance "," is used as decimal separator in some Locale)
 67  
      * @param monitor Monitor reporting the conversions
 68  
      * @param locale The Locale to use when reading numbers
 69  
      * @param listSeparator The list separator 
 70  
      */
 71  245
     public ParameterConverters(StepMonitor monitor, Locale locale, String listSeparator) {
 72  245
         this.monitor = monitor;
 73  245
         String listSepRegexp = escapeRegexPunctuation(listSeparator);
 74  245
         ParameterConverter[] defaultConverters = {
 75  
                 new NumberConverter(NumberFormat.getInstance(locale)),
 76  
                 new NumberListConverter(NumberFormat.getInstance(locale), listSepRegexp), 
 77  
                 new StringListConverter(listSepRegexp),
 78  
                 new DateConverter(), 
 79  
                 new ExamplesTableConverter()
 80  
         };
 81  245
         this.addConverters(defaultConverters);
 82  
             
 83  245
     }
 84  
     
 85  
     //TODO : This is a duplicate from RegExpPrefixCapturing
 86  
     private String escapeRegexPunctuation(String matchThis) {
 87  245
         return matchThis.replaceAll("([\\[\\]\\{\\}\\?\\^\\.\\*\\(\\)\\+\\\\])", "\\\\$1");
 88  
     }
 89  
 
 90  
     
 91  
     public ParameterConverters addConverters(ParameterConverter... converters) {
 92  245
         return addConverters(asList(converters));
 93  
     }
 94  
 
 95  
     public ParameterConverters addConverters(List<ParameterConverter> converters) {
 96  266
         this.converters.addAll(0, converters);
 97  266
         return this;
 98  
     }
 99  
 
 100  
     public Object convert(String value, Type type) {
 101  
         // check if any converters accepts type
 102  41
         for (ParameterConverter converter : converters) {
 103  171
             if (converter.accept(type)) {
 104  10
                 Object converted = converter.convertValue(value, type);
 105  10
                 monitor.convertedValueOfType(value, type, converted, converter.getClass());
 106  10
                 return converted;
 107  
             }
 108  
         }
 109  
         // default to String
 110  31
         return replaceNewlinesWithSystemNewlines(value);
 111  
     }
 112  
 
 113  
     private Object replaceNewlinesWithSystemNewlines(String value) {
 114  31
         return value.replaceAll(NEWLINES_PATTERN, SYSTEM_NEWLINE);
 115  
     }
 116  
 
 117  
     public static interface ParameterConverter {
 118  
 
 119  
         boolean accept(Type type);
 120  
 
 121  
         Object convertValue(String value, Type type);
 122  
 
 123  
     }
 124  
 
 125  
     @SuppressWarnings("serial")
 126  
     public static class ParameterConvertionFailed extends RuntimeException {
 127  
 
 128  
         public ParameterConvertionFailed(String message, Throwable cause) {
 129  4
             super(message, cause);
 130  4
         }
 131  
 
 132  
     }
 133  
 
 134  
     /**
 135  
      * <p>
 136  
      * Converts values to numbers, supporting any subclass of {@link Number}
 137  
      * (including generic Number type), and it unboxed counterpart, using a
 138  
      * {@link NumberFormat} to parse to a {@link Number} and to convert it to a
 139  
      * specific number type:
 140  
      * <ul>
 141  
      * <li>Byte, byte: {@link Number#byteValue()}</li>
 142  
      * <li>Short, short: {@link Number#shortValue()}</li>
 143  
      * <li>Integer, int: {@link Number#intValue()}</li>
 144  
      * <li>Float, float: {@link Number#floatValue()}</li>
 145  
      * <li>Long, long: {@link Number#longValue()}</li>
 146  
      * <li>Double, double: {@link Number#doubleValue()}</li>
 147  
      * <li>BigInteger: {@link BigInteger#valueOf(Long)}</li>
 148  
      * <li>BigDecimal: {@link BigDecimal#valueOf(Double)}</li></li>
 149  
      * </ul>
 150  
      * If no number format is provided, it defaults to
 151  
      * {@link NumberFormat#getInstance(Locale.ENGLISH)}.
 152  
      * <p>
 153  
      * The localized instance {@link NumberFormat#getInstance(Locale)} can be
 154  
      * used to convert numbers in specific locales.
 155  
      * </p>
 156  
      */
 157  
     public static class NumberConverter implements ParameterConverter {
 158  
 
 159  1
         private static List<Class<?>> primitiveTypes = asList(new Class<?>[] { byte.class, short.class, int.class,
 160  
                 float.class, long.class, double.class });
 161  
 
 162  
         private final NumberFormat numberFormat;
 163  
 
 164  
         public NumberConverter() {
 165  3
             this(NumberFormat.getInstance(DEFAULT_NUMBER_FORMAT_LOCAL));
 166  3
         }
 167  
 
 168  499
         public NumberConverter(NumberFormat numberFormat) {
 169  499
             this.numberFormat = numberFormat;
 170  499
         }
 171  
 
 172  
         public boolean accept(Type type) {
 173  88
             if (type instanceof Class<?>) {
 174  83
                 return Number.class.isAssignableFrom((Class<?>) type) || primitiveTypes.contains(type);
 175  
             }
 176  5
             return false;
 177  
         }
 178  
 
 179  
         public Object convertValue(String value, Type type) {
 180  
             try {
 181  112
                 Number n = numberFormat.parse(value);
 182  110
                 if (type == Byte.class || type == byte.class) {
 183  6
                     return n.byteValue();
 184  104
                 } else if (type == Short.class || type == short.class) {
 185  6
                     return n.shortValue();
 186  98
                 } else if (type == Integer.class || type == int.class) {
 187  14
                     return n.intValue();
 188  84
                 } else if (type == Float.class || type == float.class) {
 189  21
                     return n.floatValue();
 190  63
                 } else if (type == Long.class || type == long.class) {
 191  14
                     return n.longValue();
 192  49
                 } else if (type == Double.class || type == double.class) {
 193  22
                     return n.doubleValue();
 194  27
                 } else if (type == BigInteger.class) {
 195  3
                     return BigInteger.valueOf(n.longValue());
 196  24
                 } else if (type == BigDecimal.class) {
 197  12
                     return new BigDecimal(canonicalize(value));
 198  
                 } else {
 199  12
                     return n;
 200  
                 }
 201  2
             } catch (ParseException e) {
 202  2
                 throw new ParameterConvertionFailed(value, e);
 203  
             }
 204  
         }
 205  
 
 206  
         private String canonicalize(String value) {
 207  12
             char decimalPointSeparator = numberFormat.format(1.01).charAt(1);
 208  12
             int decimalPointPosition = value.lastIndexOf(decimalPointSeparator);
 209  12
             value = value.trim();
 210  12
             if (decimalPointPosition != -1) {
 211  9
                 String sf = value.substring(0, decimalPointPosition).replace("[^0-9]", "");
 212  9
                 String dp = value.substring(decimalPointPosition+1);
 213  9
                 return sf + "." + dp;
 214  
             }
 215  3
             return value.replace(' ', ',');
 216  
         }
 217  
 
 218  
     }
 219  
 
 220  
     /**
 221  
      * Converts value to list of numbers. Splits value to a list, using an
 222  
      * injectable value separator (defaulting to ",") and converts each element
 223  
      * of list via the {@link NumberConverter}, using the {@link NumberFormat}
 224  
      * provided (defaulting to {@link NumberFormat#getInstance(Locale.ENGLISH)}).
 225  
      */
 226  
     public static class NumberListConverter implements ParameterConverter {
 227  
 
 228  
         private final NumberConverter numberConverter;
 229  
         private final String valueSeparator;
 230  
 
 231  
         public NumberListConverter() {
 232  2
             this(NumberFormat.getInstance(DEFAULT_NUMBER_FORMAT_LOCAL), DEFAULT_COMMA);
 233  2
         }
 234  
 
 235  
         /**
 236  
          * @param numberFormat Specific NumberFormat to use.
 237  
          * @param valueSeparator A regexp to use as list separate
 238  
          */
 239  249
         public NumberListConverter(NumberFormat numberFormat, String valueSeparator) {
 240  249
             this.numberConverter = new NumberConverter(numberFormat);
 241  249
             this.valueSeparator = valueSeparator;
 242  249
         }
 243  
 
 244  
         public boolean accept(Type type) {
 245  38
             if (type instanceof ParameterizedType) {
 246  7
                 Type rawType = rawType(type);
 247  7
                 Type argumentType = argumentType(type);
 248  7
                 return List.class.isAssignableFrom((Class<?>) rawType)
 249  
                         && Number.class.isAssignableFrom((Class<?>) argumentType);
 250  
             }
 251  31
             return false;
 252  
         }
 253  
 
 254  
         private Type rawType(Type type) {
 255  7
             return ((ParameterizedType) type).getRawType();
 256  
         }
 257  
 
 258  
         private Type argumentType(Type type) {
 259  18
             return ((ParameterizedType) type).getActualTypeArguments()[0];
 260  
         }
 261  
 
 262  
         @SuppressWarnings("unchecked")
 263  
         public Object convertValue(String value, Type type) {
 264  11
             Class<? extends Number> argumentType = (Class<? extends Number>) argumentType(type);
 265  11
             List<String> values = trim(asList(value.split(valueSeparator)));
 266  11
             List<Number> numbers = new ArrayList<Number>();
 267  11
             for (String numberValue : values) {
 268  40
                 numbers.add((Number) numberConverter.convertValue(numberValue, argumentType));
 269  
             }
 270  10
             return numbers;
 271  
         }
 272  
 
 273  
     }
 274  
 
 275  
     /**
 276  
      * Converts value to list of String. Splits value to a list, using an
 277  
      * injectable value separator (defaults to ",") and trimming each element of
 278  
      * the list.
 279  
      */
 280  
     public static class StringListConverter implements ParameterConverter {
 281  
 
 282  
         private String valueSeparator;
 283  
 
 284  
         public StringListConverter() {
 285  1
             this(DEFAULT_COMMA);
 286  1
         }
 287  
 
 288  
         /**
 289  
          * @param numberFormat Specific NumberFormat to use.
 290  
          * @param valueSeparator A regexp to use as list separate
 291  
          */
 292  246
         public StringListConverter(String valueSeparator) {
 293  246
             this.valueSeparator = valueSeparator;
 294  246
         }
 295  
 
 296  
         public boolean accept(Type type) {
 297  35
             if (type instanceof ParameterizedType) {
 298  4
                 ParameterizedType parameterizedType = (ParameterizedType) type;
 299  4
                 Type rawType = parameterizedType.getRawType();
 300  4
                 Type argumentType = parameterizedType.getActualTypeArguments()[0];
 301  4
                 return List.class.isAssignableFrom((Class<?>) rawType)
 302  
                         && String.class.isAssignableFrom((Class<?>) argumentType);
 303  
             }
 304  31
             return false;
 305  
         }
 306  
 
 307  
         public Object convertValue(String value, Type type) {
 308  3
             if (value.trim().length() == 0)
 309  1
                 return asList();
 310  2
             return trim(asList(value.split(valueSeparator)));
 311  
         }
 312  
 
 313  
     }
 314  
 
 315  
     public static List<String> trim(List<String> values) {
 316  13
         List<String> trimmed = new ArrayList<String>();
 317  13
         for (String value : values) {
 318  45
             trimmed.add(value.trim());
 319  
         }
 320  13
         return trimmed;
 321  
     }
 322  
 
 323  
     /**
 324  
      * Parses value to a {@link Date} using an injectable {@link DateFormat}
 325  
      * (defaults to <b>new SimpleDateFormat("dd/MM/yyyy")</b>)
 326  
      */
 327  
     public static class DateConverter implements ParameterConverter {
 328  
 
 329  1
         public static final DateFormat DEFAULT_FORMAT = new SimpleDateFormat("dd/MM/yyyy");
 330  
 
 331  
         private final DateFormat dateFormat;
 332  
 
 333  
         public DateConverter() {
 334  247
             this(DEFAULT_FORMAT);
 335  247
         }
 336  
 
 337  248
         public DateConverter(DateFormat dateFormat) {
 338  248
             this.dateFormat = dateFormat;
 339  248
         }
 340  
 
 341  
         public boolean accept(Type type) {
 342  37
             if (type instanceof Class<?>) {
 343  35
                 return Date.class.isAssignableFrom((Class<?>) type);
 344  
             }
 345  2
             return false;
 346  
         }
 347  
 
 348  
         public Object convertValue(String value, Type type) {
 349  
             try {
 350  3
                 return dateFormat.parse(value);
 351  1
             } catch (ParseException e) {
 352  1
                 throw new ParameterConvertionFailed("Could not convert value " + value + " with date format "
 353  
                         + (dateFormat instanceof SimpleDateFormat ? ((SimpleDateFormat) dateFormat).toPattern() : dateFormat), e);
 354  
             }
 355  
         }
 356  
 
 357  
     }
 358  
 
 359  
     /**
 360  
      * Converts value to {@link ExamplesTable} using a {@link ExamplesTableFactory}.
 361  
      */
 362  
     public static class ExamplesTableConverter implements ParameterConverter {
 363  
 
 364  
         private final ExamplesTableFactory factory;
 365  
 
 366  
         public ExamplesTableConverter() {
 367  246
             this(new ExamplesTableFactory());
 368  246
         }
 369  
         
 370  246
         public ExamplesTableConverter(ExamplesTableFactory factory){
 371  246
             this.factory = factory;            
 372  246
         }
 373  
 
 374  
         public boolean accept(Type type) {
 375  34
             if (type instanceof Class<?>) {
 376  33
                 return ExamplesTable.class.isAssignableFrom((Class<?>) type);
 377  
             }
 378  1
             return false;
 379  
         }
 380  
 
 381  
         public Object convertValue(String value, Type type) {
 382  1
             return factory.createExamplesTable(value);
 383  
         }
 384  
 
 385  
     }
 386  
 
 387  
     /**
 388  
      * Invokes method on instance to return value.
 389  
      */
 390  
     public static class MethodReturningConverter implements ParameterConverter {
 391  
         private Object instance;
 392  
         private Method method;
 393  
 
 394  3
         public MethodReturningConverter(Method method, Object instance) {
 395  3
             this.method = method;
 396  3
             this.instance = instance;
 397  3
         }
 398  
 
 399  
         public boolean accept(Type type) {
 400  4
             if (type instanceof Class<?>) {
 401  3
                 return method.getReturnType().isAssignableFrom((Class<?>) type);
 402  
             }
 403  1
             return false;
 404  
         }
 405  
 
 406  
         public Object convertValue(String value, Type type) {
 407  
             try {
 408  3
                 return method.invoke(instance, value);
 409  1
             } catch (Exception e) {
 410  1
                 throw new ParameterConvertionFailed("Failed to invoke method " + method + " with value " + value
 411  
                         + " in " + instance, e);
 412  
             }
 413  
         }
 414  
 
 415  
     }
 416  
 }