16

I have registered a custom conversion service in a Spring 3 application. It works well for POJOs but it does not work on Lists.

For example, I convert from String to Role and it works fine, but not for List<String> to List<Role>.

All kind of ClassCastExceptions fly in the application when trying to inject Lists, no matter what they contain. The Conversion service calls the convertor for List<String> to List<Role> for all.

This makes sense if you think about it. Type erasure is the culprit here and the convertion service actually sees List to List.

Is there a way to tell the conversion service to work with generics?

What other options do I have?

JohnDoDo
  • 4,720
  • 8
  • 31
  • 47
  • If you get ClassCastExceptions at runtime, the problem is not with generics but with your code. Generics are only of use for the compiler. Generics should be usable here. Can you show some code for this conversion method? – andypandy Oct 12 '11 at 10:16
  • @andypandy: In Spring, I can register converters using the [Converter](http://static.springsource.org/spring/docs/3.0.x/javadoc-api/org/springframework/core/convert/converter/Converter.html) interface. Since that is generic, the cleanest code is with the following implementation `Converter, List>`. Off course this does not work because of type erasure. But the problem is not the code but the fact Spring applies the convertor for all Lists at runtime, even for ones that do not contain Strings or Roles. – JohnDoDo Oct 12 '11 at 10:20
  • Now I get it - thought this was your own service/service handling. Sounds like you can only register a single List converter which in turn should wrap and forward to the list converter of your choice. Not an expert on Spring Converter feature though. – andypandy Oct 12 '11 at 10:32
  • @andypandy: Yes, that's one workaround I thought of, but I don't really like `instanceof` tests or class name matches on the elements to determine the type. I searched high and low online but couldn't find a thing for lists. Before resorting to some workaround, I thought the SO community could help me out with something that maybe I have missed. – JohnDoDo Oct 12 '11 at 10:38
  • 1
    I have had last time a very deep dive into the spring conversion implementation. And it should work if you have a converter that converts from String To Role (you do not need an extra converter for List to List) **But I also found a very very strange behavior (maybe a bug) that the converter does not work if you convert from `List` to `List`. But it works if you convert from `List` to `Set`!** -- If you can confirm the same behaviour I will have a look at the code again, and maybe rise a ticket. – Ralph Oct 12 '11 at 11:51
  • Do you have control over the List implementation being used? In other words, are you creating the source List object, or is it coming from somewhere else? – RichW Oct 13 '11 at 06:22
  • @RichW: I'm creating the source list in the application context with XML, classic stuff like `xy` – JohnDoDo Oct 13 '11 at 07:51

9 Answers9

20

Another way to convert from List<A> to List<B> in Spring is to use ConversionService#convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType). Javadoc

This approach just needs a Converter<A,B>.

Call to conversionService for collection types:

List<A> source = Collections.emptyList();
TypeDescriptor sourceType = TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(A.class));
TypeDescriptor targetType = TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(B.class));
List<B> target = (List<B>) conversionService.convert(source, sourceType, targetType);

The converter:

public class ExampleConverter implements Converter<A, B> {
    @Override
    public B convert(A source) {
        //convert
    }
}
ksokol
  • 8,035
  • 3
  • 43
  • 56
15

I've encountered same problem, and by performing a little investigation find a solution (works for me). If you have two classes A and B, and have a registered converter e.g. SomeConverter implements Converter, than, to convert list of A to list of B you should do next:

List<A> listOfA = ...
List<B> listOfB = (List<B>)conversionService.convert(listOfA,
TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(A.class)),
TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(B.class)));
nndru
  • 2,057
  • 21
  • 16
  • Not sure why, but the answer marked correct at the bottom worked for me in all but one instance. I made the converter simply convert A.class to B.class, then I could pass in a list of A.class and tell it to return type List.class and it worked fine. In one instance, it would simply return the original list I passed in. Using this solution worked for me. – Bal Mar 13 '14 at 14:56
6

I'm using the Spring GenericConversionService.

The convert method in question has the following signature:

public <T> T convert(Object source, Class<T> targetType)

List<B>.class is not valid Java syntax.

This worked for me:

List<A> sourceList = ...;
conversionService.convert(sourceList, (Class<List<B>>)(Class<?>)List.class);

Got the idea from here: StackOverflow - Class object of generic class

Edit:

The above did not truly work. No compile errors, however it resulted in the sourceList not being converted, and being assigned to the targetList. This resulted in various exceptions downstream while attempting to use the targetList.

My current solution is to extend Spring's GenericConversionService and add my own convert method to handle lists.

Here's the convert method:

@SuppressWarnings({"rawtypes", "unchecked"})
public <T> List<T> convert(List<?> sourceList, Class<T> targetClass) {

    Assert.notNull(sourceList, "Cannot convert null list.");
    List<Object> targetList = new ArrayList();

    for (int i = 0; i < sourceList.size(); i++) {
        Object o = super.convert(sourceList.get(i), targetClass);
        targetList.add(o);
    }

    return (List<T>) targetList;
}

And it can be called like the following:

List<A> sourceList = ...;
List<B> targetList = conversionService.convert(sourceList, B.class);

Love to see if anyone has a better way to handle this common scenario.

Community
  • 1
  • 1
Jeff B
  • 61
  • 1
  • 3
3

Ralph was correct, it works if you have a converter that converts from A To B.

I didn't need a converter for List<A> to List<B>.

JohnDoDo
  • 4,720
  • 8
  • 31
  • 47
  • I couldn't make it work. What did you do to get it running? What Spring version were you using? I tried T convert(Object source, Class targetType) but had no success. – narduk Aug 28 '12 at 15:17
  • The converter implementation would simply be "implements . Implement it to convert one instance of ClassA to ClassB. Then call "List convertedList = conversionService.convert(listOfClassA, List.class);" This worked for me in all but one of my conversions. In the one instance it didn't work, the conversion service just kept simply returning the original list of ClassA. For this, the answer provided by nndru above solved the issue. – Bal Mar 13 '14 at 15:00
2

I had this same problem with dozer and found this: How to map collections in dozer

So to do this with spring conversion service I wrote a simple utility method:

public static <T, U> List<U> convertList(final ConversionService service, final List<T> source, final Class<U> destType) {

    final List<U> dest = new ArrayList<U>();

    if (source != null) {
        for (final T element : source) {
            dest.add(service.convert(element, destType));
        }
    }
    return dest;
}

Essentially the only difference is that this takes in a spring conversion service instead of a dozer mapper instance. You can now use this method to convert List's with type safety. This is similar to Jeff B's answer above except that you don't need to suppress warnings and this method doesn't necessarily need to belong to a given Convert...it's a utility method.

Community
  • 1
  • 1
Mike
  • 53
  • 4
2

And, if you're beyond Java 7, there's always a way to do it with streams:

List<B> listB = listA.stream().map(a -> converterService.convert(a, B.class)).collect(Collectors.toList());
skwisgaar
  • 880
  • 2
  • 13
  • 31
2

I have a work-around for you. It's not the prettiest thing in the world, but if using the Spring conversion service is important to you it might just do the trick.

As you pointed out the problem is type erasure. The best you can tell Spring via the ConversionService interface is that you can convert List to List, which to Spring makes no sense and that's why the converter doesn't work (that's a guess on my part .. I don't think it's a bug, in other words).

To do what you want you'll need to create and use a type-specific implementation of the generic interface and/or class:

public interface StringList extends List<String> { }

public class StringArrayList extends ArrayList<String> implements StringList { }

In this example you would create your list using StringArrayList instead of ArrayList and register either the implementation class (StringArrayList.class) or the interface class (StringList.class) via the ConversionService interface. It seems like you want to register the interface .. but if you only want to register the implementation class then you don't need to define the interface at all.

I hope this helps.

RichW
  • 2,004
  • 2
  • 15
  • 24
  • +1 for the idea, but wouldn't that mean that my beans will have to contain properties of type `StringList` (in my particular case `ProfileList`) instead of `List` so that the conversion service identifies the proper target type and apply the appropriate converter? – JohnDoDo Oct 14 '11 at 15:05
  • @JohnDoDo yes that's true. I promised it wasn't pretty, so thanks all the more for the +1. – RichW Oct 14 '11 at 16:20
  • @RichW The other way is to create two wrappers(classes) WrapperA, WrapperB and to put lists inside. After that you can create usual converter, pass wrappers through standard convert method. – mvb13 Sep 05 '13 at 11:08
0

One relatively clean work-around I found was to create a proxy converter class which accepts the raw object to be converted and delegates to an extended version of the Converter interface which supports a boolean canConvert semantics so as to allow the implementation to decide whether or not it can convert that source data or not.

e.g.:

The interface:

public interface SqlRowSetToCollectionConverter<T> extends Converter<SqlRowSet,Collection<T>> {
    boolean canConvert (SqlRowSet aSqlRowSet);
}

The proxy class:

public class SqlRowSetToCollectionConverterService implements Converter<SqlRowSet, Collection<?>> {

    private SqlRowSetToCollectionConverter<?>[] converters;

    public void setConverters (SqlRowSetToCollectionConverter<?>[] aConverters) {
        converters = aConverters;
    }

    @Override
    public Collection<?> convert (SqlRowSet aSource) {
        for (SqlRowSetToCollectionConverter<?> converter : converters) {
            if (converter.canConvert (aSource)) {
                return (converter.convert(aSource));
            }
        }
        return null;
    }

}

Then I would register the proxy class with Spring's Conversion Service and register with the proxy class all the extended interface implementations:

<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
    <property name="converters">
        <set>
          <bean class="com.hilton.hms.data.convert.SqlRowSetToCollectionConverterService">
             <property name="converters">
               <bean class="com.hilton.hms.data.convert.SqlRowSetToConfigCollectionConverter" />
             </property>
          </bean>
        </set>
    </property>
</bean>
okrunner
  • 3,083
  • 29
  • 22
0

GenericConversionService has now the method

@Nullable
public Object convert(@Nullable Object source, TypeDescriptor targetType) {
  return convert(source, TypeDescriptor.forObject(source), targetType);
}

so now, we can pass only one TypeDescriptor

public <S, @NonNull T> Collection<T> convert(Collection<S> source, Class<T> targetType) {
  return (Collection<@NonNull T>) requireNonNull(conversionService.convert(source, TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(targetType))));
}
OliverLJ
  • 41
  • 4