49

I'm creating a poc for using Mapstruct in my future projects.

Now I have one question how to map custom methods to a special target.

For example I have following interface mapper:

@Mapper
public interface ItemMapper {

    static ItemMapper INSTANCE = Mappers.getMapper(ItemMapper.class);

    @Mappings({ @Mapping(source = "number", target = "itemnumber"),
            @Mapping(source = "description", target = "description"),
            @Mapping(source = "itemClass.name", target = "ic"), @Mapping(source = "optionPart", target = "option"),
            @Mapping(source = "plannerCode.code", target = "plannercode"),
            @Mapping(source = "plannerCode.name", target = "planner"),
            @Mapping(source = "vendor.buyerCode.name", target = "buyer"),
            @Mapping(source = "vendor.buyerCode.code", target = "buyerCode"),
            @Mapping(source = "vendor.number", target = "vendor"),
            @Mapping(source = "vendor.name", target = "vendorName"), @Mapping(source = "pcsItem", target = "pcs"),
            @Mapping(source = "specialColourVariant", target = "specialColors"),
            @Mapping(source = "qtyBufferGreen", target = "greenLine"),
            @Mapping(source = "qtyBufferRed", target = "redine"), @Mapping(source = "leadtime", target = "leadTime"),
            @Mapping(source = "qtyStock", target = "stockAC"),
            @Mapping(source = "qtyStockSupplier", target = "stockSupplier"),
            @Mapping(source = "qtyPurchaseOrder", target = "qtyPo"),
            @Mapping(source = "qtyShopOrder", target = "qtySo"),
            @Mapping(source = "qtyFirmPlannedOrder", target = "qtyFpo"),
            @Mapping(source = "qtyForecast", target = "qtyForecast"),
            @Mapping(source = "standardCost", target = "stdCost"),
            @Mapping(source = "itemCategory.name", target = "category") })
    ItemViewModel itemToDto(Item item);

    default String locationToLocationDto(Item item) {
        return item.getItemsOnDetailedLocations().iterator().next().getLocation().getLocation();
    }

    default double locationToBinType(Item item) {
        return item.getItemsOnDetailedLocations().iterator().next().getBinType();
    }

    default double itemToLotsize(Item item) {
        double lotSize = 0;
        if (item.getLotsize() != null) {
            lotSize = item.getLotsize();
        } else if (item.getItemsOnDetailedLocations() != null && !item.getItemsOnDetailedLocations().isEmpty()) {
            ItemsOnDetailedLocation location = item.getItemsOnDetailedLocations().iterator().next();
            lotSize = location.getLotSize();
        } else {
            lotSize = 0.0;
        }
        return lotSize;
    }

    default double stockRails(Item item) {
        double value = 0;
        for (ItemsOnDetailedLocation detailedLocation : item.getItemsOnDetailedLocations()) {

            if (detailedLocation.getId().getSource().equals("RAILS")) {

                long lotSize2 = detailedLocation.getLotSize();
                long binInStock = detailedLocation.getBinInStock();

                if (binInStock != 0) {

                    value += lotSize2 * (binInStock - 0.5);
                }
            }

        }

        return value;
    }

}

In the code you can see the mapping and some default methods with other mapping in it. How can I use those methods in the Mapstruct mappings so that mapstruct uses those methods to fillin values in the fields?

Pratik Singhal
  • 6,283
  • 10
  • 55
  • 97
JimmyD
  • 2,629
  • 4
  • 29
  • 58

4 Answers4

108

As you have multiple default methods that return the same type. You would need to use Mapping method selection based on qualifiers.

What this means is that you would need to write your mapper in the following format:

@Mapper
public interface ItemMapper {

    // Omitting other mappings for clarity
    @Mapping(source = "item", target = "locationDto", qualifiedByName = "locationDto")
    @Mapping(source = "item", target = "binType", qualifiedByName = "binType")
    @Mapping(source = "item", target = "lotSize", qualifiedByName = "lotSize")
    @Mapping(source = "item", target = "stockRails", qualifiedByName = "stockRails")
    ItemViewModel itemToDto(Item item);

    @Named("locationDto")
    default String locationToLocationDto(Item item) {
        //Omitting implementation
    }

    @Named("binType")
    default double locationToBinType(Item item) {
        //Omitting implementation
    }

    @Named("lotSize")
    default double itemToLotsize(Item item) {
        //Omitting implementation
    }

    @Named("stockRails")
    default double stockRails(Item item) {
        //Omitting implementation
    }
}

Some important notes:

  • You need to use @Named from the MapStruct package
  • In source you can also specify the name of the parameter of the method
  • In qualifiedByName you need to specify the value that you have written in @Named
  • It finds the method not only by the value of @Named but also by the parameter type and return type. If it gives a compilation error that it cannot find the method by @Named it might be because the types of the parameter and the return value of the method do not fit the types of the target and the source of the @Mapping

I would strongly advise against using expressions for such complicated things. It is much more difficult to get it correct and it is more difficult for maintaining

selalerer
  • 3,766
  • 2
  • 23
  • 33
Filip
  • 19,269
  • 7
  • 51
  • 60
  • 1
    I tried removing source part from the mapping, but it did not work. My question is can we skip source parameter from the mapping and method param. – Anshul Sharma Jun 11 '20 at 04:56
  • This is a more direct usage of `@Named` than in the [MapStruct documentation](https://mapstruct.org/documentation/stable/reference/html/#selection-based-on-qualifiers). Super helpful answer for that context as well as this question, which is great! – M. Justin Jan 06 '21 at 22:25
19

Simplest way is using powerfull mapstruct @AfterMapping annotation. E.g.

@AfterMapping
public void treatAdditional(User user, @MappingTarget StudentSkillsTo studentSkillsTo) {
    System.out.println("After mapping!");
}
Grigory Kislin
  • 16,647
  • 10
  • 125
  • 197
9

Mapstruct can use similar constructions:

@Mapping(target = "name", expression = "java(user.getName() != null " +
        " ? user.getName() : "DefaultName")")

an expression can include any constructions on java e.g

 item.getItemsOnDetailedLocations()
.iterator().next().getLocation().getLocation();

if the method is large, then it is worthwhile to take it to another service and call this way

Redbore
  • 113
  • 5
  • I tried @Mapping(target = "location", expression = "java(item.getItemsOnDetailedLocations().iterator().next().getLocation().getLocation())"), @Mapping(target = "bintype", expression = "java(item.getItemsOnDetailedLocations().iterator().next().getBinType())") But without any luck. The fields are still null – JimmyD Jan 30 '18 at 13:32
  • your method, should take both objects. void itemToDto(@MappingTarget ItemViewModel itemViewModel, Item item); – Redbore Jan 30 '18 at 13:41
3

You can simply use them like following

@Mapping( target="/*Enter targetFieldName*/", expression="java( /default method which calculates target field/" )
Naveen
  • 403
  • 1
  • 6
  • 20