Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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
}
like image 729
Arian Avatar asked Jun 24 '17 18:06

Arian


1 Answers

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
like image 98
Hugues M. Avatar answered Nov 11 '22 11:11

Hugues M.