public void testWithMultiValueWithFormatter() throws Exception {
    this.tag.setPath("stringArray");
    this.tag.setItems(new Object[] {"   foo", "   bar", "   baz"});
    BeanPropertyBindingResult bindingResult =
        new BeanPropertyBindingResult(this.bean, COMMAND_NAME);
    FormattingConversionService cs = new FormattingConversionService();
    cs.addFormatterForFieldType(
        String.class,
        new Formatter<String>() {
          public String print(String object, Locale locale) {
            return object;
          }

          public String parse(String text, Locale locale) throws ParseException {
            return text.trim();
          }
        });
    bindingResult.initConversion(cs);
    getPageContext()
        .getRequest()
        .setAttribute(BindingResult.MODEL_KEY_PREFIX + COMMAND_NAME, bindingResult);

    int result = this.tag.doStartTag();
    assertEquals(Tag.SKIP_BODY, result);

    String output = getOutput();

    // wrap the output so it is valid XML
    output = "<doc>" + output + "</doc>";

    SAXReader reader = new SAXReader();
    Document document = reader.read(new StringReader(output));
    Element spanElement1 = (Element) document.getRootElement().elements().get(0);
    Element checkboxElement1 = (Element) spanElement1.elements().get(0);
    assertEquals("input", checkboxElement1.getName());
    assertEquals("checkbox", checkboxElement1.attribute("type").getValue());
    assertEquals("stringArray", checkboxElement1.attribute("name").getValue());
    assertEquals("checked", checkboxElement1.attribute("checked").getValue());
    assertEquals("   foo", checkboxElement1.attribute("value").getValue());
    Element spanElement2 = (Element) document.getRootElement().elements().get(1);
    Element checkboxElement2 = (Element) spanElement2.elements().get(0);
    assertEquals("input", checkboxElement2.getName());
    assertEquals("checkbox", checkboxElement2.attribute("type").getValue());
    assertEquals("stringArray", checkboxElement2.attribute("name").getValue());
    assertEquals("checked", checkboxElement2.attribute("checked").getValue());
    assertEquals("   bar", checkboxElement2.attribute("value").getValue());
    Element spanElement3 = (Element) document.getRootElement().elements().get(2);
    Element checkboxElement3 = (Element) spanElement3.elements().get(0);
    assertEquals("input", checkboxElement3.getName());
    assertEquals("checkbox", checkboxElement3.attribute("type").getValue());
    assertEquals("stringArray", checkboxElement3.attribute("name").getValue());
    assertNull("not checked", checkboxElement3.attribute("checked"));
    assertEquals("   baz", checkboxElement3.attribute("value").getValue());
  }
  /**
   * Registers a simple formatter.
   *
   * @param clazz class handled by this formatter
   * @param formatter the formatter to register
   */
  public static <T> void register(final Class<T> clazz, final SimpleFormatter<T> formatter) {
    conversion.addFormatterForFieldType(
        clazz,
        new org.springframework.format.Formatter<T>() {

          public T parse(String text, Locale locale) throws java.text.ParseException {
            return formatter.parse(text, locale);
          }

          public String print(T t, Locale locale) {
            return formatter.print(t, locale);
          }

          public String toString() {
            return formatter.toString();
          }
        });
  }
  /** Converter for String -> Option and Option -> String */
  private static <T> void registerOption() {
    conversion.addConverter(
        new GenericConverter() {

          public Object convert(
              Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
            if (sourceType.getObjectType().equals(String.class)) {
              // From String to Option
              Object element =
                  conversion.convert(source, sourceType, targetType.getElementTypeDescriptor());
              if (element == null) {
                return new None();
              } else {
                return new Some<Object>(element);
              }
            } else if (targetType.getObjectType().equals(String.class)) {
              // Fromt Option to String
              if (source == null) return "";

              Option<?> opt = (Option) source;
              if (!opt.isDefined()) {
                return "";
              } else {
                return conversion.convert(
                    source, sourceType.getElementTypeDescriptor(), targetType);
              }
            }
            return null;
          }

          public Set<GenericConverter.ConvertiblePair> getConvertibleTypes() {
            Set<ConvertiblePair> result = new HashSet<ConvertiblePair>();
            result.add(new ConvertiblePair(Option.class, String.class));
            result.add(new ConvertiblePair(String.class, Option.class));
            return result;
          }
        });
  }
  /**
   * Registers an annotation-based formatter.
   *
   * @param clazz class handled by this formatter
   * @param formatter the formatter to register
   */
  @SuppressWarnings("unchecked")
  public static <A extends Annotation, T> void register(
      final Class<T> clazz, final AnnotationFormatter<A, T> formatter) {
    final Class<? extends Annotation> annotationType =
        (Class<? extends Annotation>)
            GenericTypeResolver.resolveTypeArguments(
                formatter.getClass(), AnnotationFormatter.class)[0];

    conversion.addConverter(
        new ConditionalGenericConverter() {
          public Set<GenericConverter.ConvertiblePair> getConvertibleTypes() {
            Set<GenericConverter.ConvertiblePair> types =
                new HashSet<GenericConverter.ConvertiblePair>();
            types.add(new GenericConverter.ConvertiblePair(clazz, String.class));
            return types;
          }

          public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
            return (sourceType.getAnnotation(annotationType) != null);
          }

          public Object convert(
              Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
            final A a = (A) sourceType.getAnnotation(annotationType);
            Locale locale = LocaleContextHolder.getLocale();
            try {
              return formatter.print(a, (T) source, locale);
            } catch (Exception ex) {
              throw new ConversionFailedException(sourceType, targetType, source, ex);
            }
          }

          public String toString() {
            return "@"
                + annotationType.getName()
                + " "
                + clazz.getName()
                + " -> "
                + String.class.getName()
                + ": "
                + formatter;
          }
        });

    conversion.addConverter(
        new ConditionalGenericConverter() {
          public Set<GenericConverter.ConvertiblePair> getConvertibleTypes() {
            Set<GenericConverter.ConvertiblePair> types =
                new HashSet<GenericConverter.ConvertiblePair>();
            types.add(new GenericConverter.ConvertiblePair(String.class, clazz));
            return types;
          }

          public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
            return (targetType.getAnnotation(annotationType) != null);
          }

          public Object convert(
              Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
            final A a = (A) targetType.getAnnotation(annotationType);
            Locale locale = LocaleContextHolder.getLocale();
            try {
              return formatter.parse(a, (String) source, locale);
            } catch (Exception ex) {
              throw new ConversionFailedException(sourceType, targetType, source, ex);
            }
          }

          public String toString() {
            return String.class.getName()
                + " -> @"
                + annotationType.getName()
                + " "
                + clazz.getName()
                + ": "
                + formatter;
          }
        });
  }