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