ModelMapper: Ensure that method has zero parameters and does not return void
Asked Answered
K

1

12

I have the following configuration for the model mapper to convert an instance of User class to an instance of ExtendedGetUserDto.

    public ExtendedGetUserDto convertToExtendedDto(User user) {
        PropertyMap<User, ExtendedGetUserDto> userMap = new PropertyMap<User, ExtendedGetUserDto>() {
            protected void configure() {
                map().setDescription(source.getDescription());
                map().setId(source.getId());
//              map().setReceivedExpenses(
//                      source.getReceivedExpenses()
//                              .stream()
//                              .map(expense -> expenseDtoConverter.convertToDto(expense))
//                              .collect(Collectors.toSet())
//                      );
                Set<GetInvitationDto> result = new HashSet<GetInvitationDto>();
                for (Invitation inv: source.getReceivedInvitations()) {
                    System.out.println("HELLO");
                    //result.add(null);
                }
                //map().setReceivedInvitations(result);
            }
        };
        modelMapper.addMappings(userMap);
        return modelMapper.map(user, ExtendedGetUserDto.class);
    }

Before commenting out setReceivedExpense I received this error:

org.modelmapper.ConfigurationException: ModelMapper configuration errors:

1) Invalid source method java.util.stream.Stream.map(). Ensure that method has zero parameters and does not return void.

2) Invalid source method java.util.stream.Stream.collect(). Ensure that method has zero parameters and does not return void.

2 errors

After spending some time and not finding the root cause, I tried to delete all suspecious cyclic dependencies in the DTOs (I have GetUserDto referenced in GetExpenseDto, the returning result of expenseDtoConverter) I still receive the same error, I commented out map().setReceivedExpenses (as you can see in the code) and replaced it with simple for loop.

I get the following error:

1) Invalid source method java.io.PrintStream.println(). Ensure that method has zero parameters and does not return void.

Why do I receive these errors ?

Edit 1

User.java

@Entity
@Table(name="User")
public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name="id")
    private long id;

    @Column(name = "name")
    private String name;

    @Size(min=15, max=15)
    @Column(name="image_id")
    private String imageId;

    @Size(max=100)
    @Column(name="description")
    private String description;

    @OneToMany(mappedBy="admin")
    private Set<Group> ownedGroups;

    @ManyToMany(mappedBy="members")
    private Set<Group> memberGroups;

    @OneToMany(cascade = CascadeType.ALL, fetch=FetchType.EAGER, mappedBy="owner")
    private Set<Expense> ownedExpenses;

    @ManyToMany(cascade = CascadeType.REFRESH, fetch=FetchType.EAGER)
    private Set<Expense> receivedExpenses;

    @OneToMany(cascade=CascadeType.ALL)
    private Set<Invitation> ownedInvitations;

    @OneToMany(cascade=CascadeType.ALL)
    private Set<Invitation> receivedInvitations;
    //setters and getters for attributes
}

ExtendedGetUserDto.java

public class ExtendedGetUserDto extends GetUserDto {

    private static final long serialVersionUID = 1L;

    private Set<GetInvitationDto> receivedInvitations;
    private Set<GetExpenseDto> receivedExpenses;
    private Set<GetExpenseDto> ownedExpenses;
    private Set<GetGroupDto> ownedGroups;
    private Set<GetGroupDto> memberGroups;
    //setters and getters for attributes
}
Kasper answered 24/6, 2017 at 18:36 Comment(2)
Could you add your classes: ExtendedGetUserDto and User?Barbitone
@Barbitone I added the classes you mentionedKasper
C
32

You receive these errors because PropertyMap restricts what you can do inside configure().

In the Javadoc:

PropertyMap uses an Embedded Domain Specific Language (EDSL) to define how source and destination methods and values map to each other. The Mapping EDSL allows you to define mappings using actual code that references the source and destination properties you wish to map. Usage of the EDSL is demonstrated in the examples below.

Technically it involves bytecode analysis, manipulation and proxying, and it expects Java method invocations that fit within this EDSL. This clever trick allows ModelMapper to record your mapping instructions, and replay them at will.

To get a glimpse in the library source code: Error you get is invalidSourceMethod, thrown here in ExplicitMappingVisitor where ObjectMapper visits and instruments the code of your configure method, using ASM library.

The following example is a freestanding runnable example, that should help clarify. I invite you to copy it in ModelMapperTest.java and actually run it, then switch the comments inside configure() to reproduce the error:

import org.modelmapper.ModelMapper;
import org.modelmapper.PropertyMap;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class ModelMapperTest {

    public static void main(String[] args) {
        PropertyMap<Foo, FooDTO> propertyMap = new PropertyMap<Foo, FooDTO>() {
            protected void configure() {
                /* This is executed exactly ONCE, to "record" the mapping instructions.
                 * The bytecode of this configure() method is analyzed to produce new mapping code,
                 * a new dynamically-generated class with a method that will basically contain the same instructions
                 * that will be "replayed" each time you actually map an object later.
                 * But this can only work if the instructions are simple enough (ie follow the DSL).
                 * If you add non-compliant code here, it will break before "configure" is invoked.
                 * Non-compliant code is supposedly anything that does not follow the DSL.
                 * In practice, the framework only tracks what happens to "map()" and "source", so
                 * as long as print instructions do not access the source or target data (like below),
                 * the framework will ignore them, and they are safe to leave for debug. */
                System.out.println("Entering configure()");
                // This works
                List<String> things = source.getThings();
                map().setThingsCSVFromList(things);
                // This would fail (not because of Java 8 code, but because of non-DSL code that accesses the data)
                // String csv = things.stream().collect(Collectors.joining(","));
                // map().setThingsCSV(csv);
                System.out.println("Exiting configure()");
            }
        };
        ModelMapper modelMapper = new ModelMapper();
        modelMapper.addMappings(propertyMap);
        for (int i=0; i<5; i++) {
            Foo foo = new Foo();
            foo.setThings(Arrays.asList("a"+i, "b"+i, "c"+i));
            FooDTO dto = new FooDTO();
            modelMapper.map(foo, dto); // The configure method is not re-executed, but the dynamically generated mapper method is.
            System.out.println(dto.getThingsCSV());
        }
    }

    public static class Foo {

        List<String> things;

        public List<String> getThings() {
            return things;
        }

        public void setThings(List<String> things) {
            this.things = things;
        }

    }

    public static class FooDTO {

        String thingsCSV;

        public String getThingsCSV() {
            return thingsCSV;
        }

        public void setThingsCSV(String thingsCSV) {
            this.thingsCSV = thingsCSV;
        }

        public void setThingsCSVFromList(List<String> things) {
            setThingsCSV(things.stream().collect(Collectors.joining(",")));
        }

    }

}

If you execute it as is, you get:

Entering configure()
Exiting configure()
a0,b0,c0
a1,b1,c1
a2,b2,c2
a3,b3,c3
a4,b4,c4

So, configure() is executed exactly once to record mapping instructions, and then the generated mapping code (not configure() itself) is replayed 5 times, once for each object mapping.

If you comment out the lines with map().setThingsCSVFromList(things) within configure(), and then uncomment the 2 lines below "This would fail", you get:

Exception in thread "main" org.modelmapper.ConfigurationException: ModelMapper configuration errors:

1) Invalid source method java.util.stream.Stream.collect(). Ensure that method has zero parameters and does not return void.

In short, you cannot execute complex custom logic directly within PropertyMap.configure(), but you can invoke methods that do. This is because the framework only needs to instrument the parts of the bytecode that deal with pure mapping logic (ie the DSL), it does not care what happens within those methods.

Solution

(A -- legacy, for Java 6/7) strictly restrict the content of configure as required by DSL. For example, move your "special needs" (logging, collecting logic, etc) to dedicated method in the DTO itself.

In your case it might be more work to move that logic elsewhere, but the idea is there.

Please note the doc implies PropertyMap.configure and its DSL was mostly useful with Java 6/7, but Java 8 and lambdas now allow elegant solutions that have the advantage of not requiring bytecode manipulation magic.

(B -- Java 8) Check out other options, such as Converter.

Here is another example (using same data classes as above, and a Converter for the whole type because that suits my example better, but you could do that property-by-property):

    Converter<Foo, FooDTO> converter = context -> {
        FooDTO dto = new FooDTO();
        dto.setThingsCSV(
                context.getSource().getThings().stream()
                        .collect(Collectors.joining(",")));
        return dto;
    };
    ModelMapper modelMapper = new ModelMapper();
    modelMapper.createTypeMap(Foo.class, FooDTO.class)
            .setConverter(converter);
    Foo foo = new Foo();
    foo.setThings(Arrays.asList("a", "b", "c"));
    FooDTO dto = modelMapper.map(foo, FooDTO.class);
    System.out.println(dto.getThingsCSV()); // a,b,c
Chez answered 27/6, 2017 at 8:20 Comment(5)
Furthermore, if you want to add complex custom logic, you could use a Converter, but never use a PropertyMapBarbitone
Yes, the doc implies PropertyMap.configure and its DSL is only useful with Java 6/7, but with Java 8 you have other options, such as Converter. Thanks, I should edit, because I'm pointing to a "bad" solution.Chez
@HuguesMoreau but in my sample code I don't use Java 8 specific methods. I use for system.out.println etc. What if I use Converter? can I convert another DTO inside a converter, just like how I call .map(expense -> expenseDtoConverter.convertToDto(expense)) ?Kasper
The problem is not that you use Java 8 methods, sorry if that was unclear. The problem is that you use methods that do not comply with the restrictions enforced by PropertyMap.configure() (streams or println, no matter). I did not develop how you would use a converter because honestly I don't know, the doc invites you to use that if you have Java 8. But you can continue using PropertyMap.configure() as shown in answer, provided you wrap all non-compliant code to compliant methods (as in my example).Chez
@ArianHosseinzadeh I edited the text and the code. If the reasons why it fails are not crystal clear, please copy-paste the code I provided and actually run it. Then about how to use converters, I think that yes that's the advantage of Java8-based solutions, there is no more bytecode manipulation magic, no DSL, no weird constraints (at least as far as I can tell)Chez

© 2022 - 2024 — McMap. All rights reserved.