Mapstruct: exclude all field by superclass

Viewed 206

I have standard entities and dto's.

Entities:

public class Type extends DefaultEntity {
  int id;
  String name
  ...
}
            
public class Attribute extends DefaultEntity{
  int id;
  String name; 
  Type type;
  ...
}

and DTO's

public class TypeDto extends DefaultDto {
  int id;
  String name;
  ...
}
        
public class AttributeDto extends DefaultDto{
  int id;
  String name; 
  TypeDto type;
  ....
}

when I create this Mapper:

@Mapper
public interface AttributeDtoConverter {
  AttributeDto convert(Attribute source);
}

Mapstruct generates:

public class AttributeDtoConverterImpl implements AttributeDtoConverter {
    
  @Override
  public AttributeDto convert(Attribute source) {
    if ( source == null ) {
        return null;
    }

    AttributeDto attributeDto = new AttributeDto();

    attributeDto.setId( source.getId() );
    attributeDto.setName( source.getName() );
    attributeDto.setCode( source.getCode() );
    attributeDto.setType( typeToTypeDto( source.getType() ) );

    return attributeDto;
  }

  // Here is the problem
  protected TypeDto typeToTypeDto(Type type) {
    if ( type== null ) {
        return null;
    }

    TypeDto dictionaryDto = new TypeDto();

    typeDto.setId( type.getId() );
    typeDto.setName( type.getName() );

    return typeDto;
  }
}

I want to exclude from mapping all properties of target object if they are subclasses of DefaultDto (or exclude all properties of source object if they are subclasses of DefaultEntity).

I know I can exclude every property by name @Mapping(target="type", ignore=true), but for large classes with much more dependencies it will we very annoying. And changing model with this approach becomes hell.

Can I somehow exclude all properties by their superclass?

Update I tried @Condition:

@Mapper
public interface AttributeDtoConverter {
  AttributeDto convert(Attribute source);

  @Condition
  default boolean needToMap(Object value){
    return ! (value instanceof DefaultDto || value instanceof DefaultEntity);
  }
}

It works but I don't like how the generated code looks like:

public class AttributeDtoConverterImpl implements AttributeDtoConverter {
    
    @Override
    public AttributeDto convert(Attribute source) {
        if ( source == null ) {
            return null;
        }

        AttributeDto attributeDto = new AttributeDto();

        if ( needToMap( attributeDto.getId() ) ) {
          attributeDto.setId( source.getId() );
        }
        
          if ( needToMap( attributeDto.getName() ) ) {
            attributeDto.setName( source.getName() );
        }
        if ( needToMap( attributeDto.getCode() ) ) {
          attributeDto.setCode( source.getCode() );
        }
        if ( needToMap( attributeDto.getType() ) ) {
          attributeDto.setType( typeToTypeDto( source.getType() ) );
        }

        return attributeDto;
    }
...
}
2 Answers

I don't know if it will work for your use case, because the solution is global, not specific to a certain mapping, but probably a good approach would be using a custom MappingExclusionProvider.

The Mapstruct documentation indicates:

MapStruct offers the possibility to override the MappingExclusionProvider via the Service Provider Interface (SPI). A nice example is to not allow MapStruct to create an automatic sub-mapping for a certain type, i.e. MapStruct will not try to generate an automatic sub-mapping method for an excluded type.

For example:

import java.util.regex.Pattern;
import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement;

import org.mapstruct.ap.spi.MappingExclusionProvider;

public class CustomMappingExclusionProvider implements MappingExclusionProvider {
    private static final Pattern JAVA_JAVAX_PACKAGE = Pattern.compile( "^javax?\\..*" );

    @Override
    public boolean isExcluded(TypeElement typeElement) {
        Name name = typeElement.getQualifiedName();
        return name.length() != 0 && ( JAVA_JAVAX_PACKAGE.matcher( name ).matches() ||
            name.toString().equals( "your.pkg.DefaultDto" )||
            name.toString().equals( "your.pkg.DefaultEntity" ) );
    }
}

Two things should be pointed out here:

  • First, as the Mapstruct default implementation, your custom exclusion mapper should exclude all types under the java or javax packages. As indicated in the docs, this means that MapStruct will not try to generate an automatic sub-mapping method between some custom type and some type declared in the Java class library.
  • Second, as explained again in the docs, to register the service your need to:

To use a custom SPI implementation, it must be located in a separate JAR file together with a file named after the SPI (e.g. org.mapstruct.ap.spi.AccessorNamingStrategy) in META-INF/services/ with the fully qualified name of your custom implementation as content (e.g. org.mapstruct.example.CustomAccessorNamingStrategy). This JAR file needs to be added to the annotation processor classpath (i.e. add it next to the place where you added the mapstruct-processor jar).

In your case it means to define a file named org.mapstruct.ap.spi.MappingExclusionProvider under META-INF/services/ with the fully qualified name of your custom MappingExclusionProvider.

What about using @BeforeMapping and @AfterMapping hooks ? The map will be done anyways, so perhaps is not the best solution, but no exclusion seems possible due to mapstruct features on exclusion and your requirement about applying only to some converters, but mapping can be done with non conflicting, i.e. null or empty values if those values are modified in any of the hooks. Here an example, source from mapstruct doc: https://mapstruct.org/documentation/stable/reference/html/#customizing-mappings-with-before-and-after

@Mapper
public abstract class VehicleMapper {
    @BeforeMapping
    protected void flushEntity(AbstractVehicle vehicle) {

    }

    @AfterMapping
    protected void fillTank(AbstractVehicle vehicle, @MappingTarget AbstractVehicleDto result) {
        result.fuelUp( new Fuel( vehicle.getTankCapacity(), vehicle.getFuelType() ) );
    }

    public abstract CarDto toCarDto(Car car);
}

// Generates something like this:
public class VehicleMapperImpl extends VehicleMapper {
    public CarDto toCarDto(Car car) {
        flushEntity( car );

        if ( car == null ) {
            return null;
        }

        CarDto carDto = new CarDto();
        // attributes mapping ...

        fillTank( car, carDto );

        return carDto;
    }
}
Related