Core JavaJava

@Context Complex Source Mappings Example

1. Introduction

The org.mapstruct.Context annotation from MapStruct, a Java annotation processor for generating type-safe bean mapping code, marks a parameter of a method to be treated as mapping context. The @context complex source mappings inject context objects into a mapping method. In this example, I will demonstrate @context complex source mappings via @Context, @AfterMapping, @BeforeMapping, @ObjectFactory, Expression, and QualifiedByName.

2. Setup

In this step, I will create a Gradle project along with Lombok and MapStruct libraries.

build.gradle

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.4.5'
	id 'io.spring.dependency-management' version '1.1.7'
}

group = 'com.zheng.mapstruct.demo'
version = '0.0.1-SNAPSHOT'

java {
	toolchain {
		languageVersion = JavaLanguageVersion.of(17)
	}
}

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter'
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
	
	implementation 'org.mapstruct:mapstruct:1.5.5.Final' 	
    annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'
	
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
	useJUnitPlatform()
}

3. Java POJO & External Service

3.1 Source

In this step, I will create a Source.java that has an Integer id and String fullName fields.

Source.java

package com.zheng.mapstruct.demo.data;

import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
public class Source {
	private String fullName;

	private Integer id;

}

3.2 Target

In this step, I will create a Target.java that has an Integer id, String lookUpValue, String firstName,and String lastName, fields. The String lookUpValue value is set by the ExternalService.findNameById method and the firstName and lastName are set by the NameContext.splitFullName method.

Target.java

package com.zheng.mapstruct.demo.data;

import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
public class Target {
	private String firstName;
	private Integer id;

	private String lastName;
	private String lookUpValue;
}

3.3 ExternalService

In this step, I will create an ExternalService.java that has the findNameById method to find the name based on its id. It will be used as a parameter in the Mapper.map() method in step 4.

ExternalService.java

package com.zheng.mapstruct.demo.service;

import org.springframework.stereotype.Service;

@Service
public class ExternalService {

	public String findNameById(int someId) {
		if (someId < 0) {
			throw new RuntimeException("Negative value is invalid!");
		}
		return "lookUpValue_" + someId;
	}

}

3.4 LookUpContext

In this step, I will create a LookUpContext.java that has a resolveLookUpValue method to set the lookUpValue via the externalService.findNameById method. It will be used as a parameter in the Mapper.map() method in step 4.

LookUpContext.java

package com.zheng.mapstruct.demo.service;

import org.springframework.stereotype.Component;

import com.zheng.mapstruct.demo.data.Target;

@Component
public class LookUpContext {

	private final ExternalService service;

	public LookUpContext(ExternalService service) {
		super();
		this.service = service;
	}

	public Target resolveLookUpValue(Integer someId) {
		Target ret = new Target();
		if (someId == null) {
			throw new RuntimeException("Null is invalid.");
		}
		ret.setId(someId);
		ret.setLookUpValue(service.findNameById(someId.intValue()));
		return ret;
	}

}

3.5 NameContext

In this step, I will create a NameContext.java that has a splitFullName method to split the fullName into the firstName and lastName. It will be used as a parameter in the Mapper.map() method in step 4.

NameContext.java

package com.zheng.mapstruct.demo.service;

import org.springframework.stereotype.Component;

@Component
public class NameContext {
	public String[] splitFullName(String fullName) {
		return fullName != null ? fullName.trim().split("\\s+", 2) : new String[] { "", "" };
	}

}

4. @Context Annotation Complex Source Mappings

MapStruct supports both interface and abstract classes. It’s better to define the Mapper as an Interface when All methods are either simple mappings or delegating to other mappers or default methods. However, when there is custom mapping logic or injecting dependencies via fields or constructors, then defining Mapper as the abstract class is the better choice. In this step, I will create two interfaces and three Abstract classes to demonstrate @context annotation complex source mappings.

4.1 Mapper Interface with @Context Expression

In this step, I will create a MapperWithExpression.java interface that utilizes @Context and calls its findNameById method to set the target‘s lookUpValue data via the expression attribute.

MapperWithExpression.java

package com.zheng.mapstruct.demo.mapper;

import org.mapstruct.Context;
import org.mapstruct.InjectionStrategy;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;

import com.zheng.mapstruct.demo.data.Source;
import com.zheng.mapstruct.demo.data.Target;
import com.zheng.mapstruct.demo.service.ExternalService;
import com.zheng.mapstruct.demo.service.NameContext;

@Mapper(componentModel = "spring", injectionStrategy = InjectionStrategy.CONSTRUCTOR)
public interface MapperWithExpression {
	MapperWithExpression INSTANCE = Mappers.getMapper(MapperWithExpression.class);

	@Mapping(target = "lookUpValue", expression = "java(context.findNameById(source.getId()))")
	@Mapping(target = "firstName", expression = "java(nameContext.splitFullName(source.getFullName())[0])")
	@Mapping(target = "lastName", expression = "java(nameContext.splitFullName(source.getFullName())[1])")
	Target map(Source source, @Context ExternalService context, @Context NameContext nameContext);

}
  • Line 18: this expression accesses the context.findNameById method to set the lookUpValue.
  • Line 19, 20: this expression accesses the nameContext.splitFullName method to set the firstName and lastName.
  • Line 21: the map method injects two contexts for complex mapping logic.

4.2 Mapper Interface with @Context QualifiedByName

In this step, I will create a MapperWithQualifiedByName.java interface that utilizes @Context and calls its findNameById method to set the target‘s lookUpValue data via the qualifiedByName attribute.

MapperWithQualifiedByName.java

package com.zheng.mapstruct.demo.mapper;

import org.mapstruct.Context;
import org.mapstruct.InjectionStrategy;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Named;
import org.mapstruct.factory.Mappers;

import com.zheng.mapstruct.demo.data.Source;
import com.zheng.mapstruct.demo.data.Target;
import com.zheng.mapstruct.demo.service.ExternalService;

@Mapper(componentModel = "spring", injectionStrategy = InjectionStrategy.CONSTRUCTOR)
public interface MapperWithQualifiedByName {
	MapperWithQualifiedByName INSTANCE = Mappers.getMapper(MapperWithQualifiedByName.class);

	@Named("extractFirstName")
	static String extractFirstName(String fullName) {
		if (fullName == null)
			return "";
		return fullName.trim().split("\\s+", 2)[0];
	}

	@Named("extractLastName")
	static String extractLastName(String fullName) {
		if (fullName == null)
			return "";
		String[] parts = fullName.trim().split("\\s+", 2);
		return parts.length > 1 ? parts[1] : "";
	}

	@Mapping(source = "id", target = "lookUpValue", qualifiedByName = "lookUpValueMapper")
	@Mapping(target = "firstName", source = "fullName", qualifiedByName = "extractFirstName")
	@Mapping(target = "lastName", source = "fullName", qualifiedByName = "extractLastName")
	Target map(Source source, @Context ExternalService context);

	@Named("lookUpValueMapper")
	default String mapIdToName(int id, @Context ExternalService context) {
		return context.findNameById(id);
	}

}
  • Line 36: the map method injects context for complex mapping logic.
  • Line 39: the default mapIdToName method injects context for complex mapping logic.

4.3 Abstract Mapper Class with @Context @BeforeMapping

In this step, I will create an abstract class MapperWithBeforeMapping.java that utilizes @Context and calls its findNameById method to set the target‘s lookUpValue data via the @BeforeMapping annotation. The method setLookupValue marked with @BeforeMapping is generated at the first line of mapping logic.

MapperWithBeforeMapping.java

package com.zheng.mapstruct.demo.mapper;

import org.mapstruct.BeforeMapping;
import org.mapstruct.Context;
import org.mapstruct.InjectionStrategy;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import org.mapstruct.ReportingPolicy;
import org.mapstruct.factory.Mappers;

import com.zheng.mapstruct.demo.data.Source;
import com.zheng.mapstruct.demo.data.Target;
import com.zheng.mapstruct.demo.service.ExternalService;
import com.zheng.mapstruct.demo.service.NameContext;

@Mapper(componentModel = "spring", injectionStrategy = InjectionStrategy.CONSTRUCTOR, unmappedTargetPolicy = ReportingPolicy.IGNORE)
public abstract class MapperWithBeforeMapping {

	public static final MapperWithBeforeMapping INSTANCE = Mappers.getMapper(MapperWithBeforeMapping.class);

	public abstract Target map(Source user, @Context ExternalService context, @Context NameContext nameContext);

	@BeforeMapping
	protected void setLookupValue(Source src, @MappingTarget Target target, @Context ExternalService context) {
		String lookupName = context.findNameById(src.getId());
		target.setLookUpValue(lookupName);

	}

	@BeforeMapping
	protected void splitFullName(Source user, @MappingTarget Target dto, @Context NameContext nameContext) {
		String[] names = nameContext.splitFullName(user.getFullName());
		dto.setFirstName(names[0]);
		dto.setLastName(names.length > 1 ? names[1] : "");
	}
}
  • Line 21: the map method injects two contexts for complex mapping logic.
  • Line 23, 30: the @BeforeMapping annotation is marked.

4.4 Abstract Mapper Class with @Context @AfterMapping

In this step, I will create an abstract class MapperWithAfterMapping.java that utilizes @Context and calls its findNameById method to set the target‘s lookUpValue data via the @AfterMapping annotation. The method setLookupValue marked with @AfterMapping is generated at the last line of mapping logic.

MapperWithAfterMapping.java

package com.zheng.mapstruct.demo.mapper;

import org.mapstruct.AfterMapping;
import org.mapstruct.Context;
import org.mapstruct.InjectionStrategy;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import org.mapstruct.ReportingPolicy;
import org.mapstruct.factory.Mappers;

import com.zheng.mapstruct.demo.data.Source;
import com.zheng.mapstruct.demo.data.Target;
import com.zheng.mapstruct.demo.service.ExternalService;
import com.zheng.mapstruct.demo.service.NameContext;

@Mapper(componentModel = "spring", injectionStrategy = InjectionStrategy.CONSTRUCTOR, unmappedTargetPolicy = ReportingPolicy.IGNORE)
public abstract class MapperWithAfterMapping {

	public static final MapperWithAfterMapping INSTANCE = Mappers.getMapper(MapperWithAfterMapping.class);

	public abstract Target map(Source user, @Context ExternalService context, @Context NameContext nameContext);

	@AfterMapping
	protected void setLookupValue(Source src, @MappingTarget Target target, @Context ExternalService context) {

		String lookupName = context.findNameById(src.getId());
		target.setLookUpValue(lookupName);

	}

	@AfterMapping
	protected void splitFullName(Source user, @MappingTarget Target dto, @Context NameContext nameContext) {
		String[] names = nameContext.splitFullName(user.getFullName());
		dto.setFirstName(names[0]);
		dto.setLastName(names.length > 1 ? names[1] : "");
	}

}
  • Line 21: the map method injects two contexts for complex mapping logic.
  • Line 23, 31: the @AfterMapping annotation is marked.

4.5 Abstract Mapper Class with @Context @ObjectFactory

In this step, I will create an abstract class MapperWithObjectFactory.java that utilizes @Context and calls its resolveLookUpValue method to create the target object and set its lookUpValue data via the @ObjectFactory annotation.

MapperWithObjectFactory.java

package com.zheng.mapstruct.demo.mapper;

import org.mapstruct.Context;
import org.mapstruct.InjectionStrategy;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.ObjectFactory;
import org.mapstruct.ReportingPolicy;
import org.mapstruct.factory.Mappers;

import com.zheng.mapstruct.demo.data.Source;
import com.zheng.mapstruct.demo.data.Target;
import com.zheng.mapstruct.demo.service.LookUpContext;
import com.zheng.mapstruct.demo.service.NameContext;

@Mapper(componentModel = "spring", injectionStrategy = InjectionStrategy.CONSTRUCTOR, unmappedTargetPolicy = ReportingPolicy.IGNORE)
public abstract class MapperWithObjectFactory {

	public static final MapperWithObjectFactory INSTANCE = Mappers.getMapper(MapperWithObjectFactory.class);

	@ObjectFactory
	protected Target createTarget(Source dto, @Context LookUpContext context, @Context NameContext nameContext) {
		Target target = context.resolveLookUpValue(dto.getId());
		String[] names = nameContext.splitFullName(dto.getFullName());
		target.setFirstName(names[0]);
		if (names.length > 1) {		
			target.setLastName(names[1]);
		}
		return target;
	}

	@Mapping(target = "id", ignore = true)
	public abstract Target map(Source user, @Context LookUpContext context, @Context NameContext nameContext);
}
  • Line 21: the map method injects two contexts for complex mapping logic.
  • Line 33: the @ObjectFactory annotation is marked.

5. Generate Implementation Class

5.1 MapperWithExpressionImpl

In this step, I will print the generated MapperWithExpressionImpl.java class.

MapperWithExpressionImpl.java

package com.zheng.mapstruct.demo.mapper;

import com.zheng.mapstruct.demo.data.Source;
import com.zheng.mapstruct.demo.data.Target;
import com.zheng.mapstruct.demo.service.ExternalService;
import com.zheng.mapstruct.demo.service.NameContext;
import javax.annotation.processing.Generated;
import org.springframework.stereotype.Component;

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2025-05-08T16:21:59-0500",
    comments = "version: 1.5.5.Final, compiler: IncrementalProcessingEnvironment from gradle-language-java-8.13.jar, environment: Java 17 (Oracle Corporation)"
)
@Component
public class MapperWithExpressionImpl implements MapperWithExpression {

    @Override
    public Target map(Source source, ExternalService context, NameContext nameContext) {
        if ( source == null ) {
            return null;
        }

        Target target = new Target();

        target.setId( source.getId() );

        target.setLookUpValue( context.findNameById(source.getId()) );
        target.setFirstName( nameContext.splitFullName(source.getFullName())[0] );
        target.setLastName( nameContext.splitFullName(source.getFullName())[1] );

        return target;
    }
}

5.2 MapperWithQualifiedByNameImpl

In this step, I will print the generated MapperWithQualifiedByNameImpl.java class.

MapperWithQualifiedByNameImpl.java

package com.zheng.mapstruct.demo.mapper;

import com.zheng.mapstruct.demo.data.Source;
import com.zheng.mapstruct.demo.data.Target;
import com.zheng.mapstruct.demo.service.ExternalService;
import javax.annotation.processing.Generated;
import org.springframework.stereotype.Component;

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2025-05-08T16:21:59-0500",
    comments = "version: 1.5.5.Final, compiler: IncrementalProcessingEnvironment from gradle-language-java-8.13.jar, environment: Java 17 (Oracle Corporation)"
)
@Component
public class MapperWithQualifiedByNameImpl implements MapperWithQualifiedByName {

    @Override
    public Target map(Source source, ExternalService context) {
        if ( source == null ) {
            return null;
        }

        Target target = new Target();

        if ( source.getId() != null ) {
            target.setLookUpValue( mapIdToName( source.getId().intValue(), context ) );
        }
        target.setFirstName( MapperWithQualifiedByName.extractFirstName( source.getFullName() ) );
        target.setLastName( MapperWithQualifiedByName.extractLastName( source.getFullName() ) );
        target.setId( source.getId() );

        return target;
    }
}

5.3 MapperWithBeforeMappingImpl

In this step, I will print the generated MapperWithBeforeMappingImpl.java class.

MapperWithBeforeMappingImpl.java

package com.zheng.mapstruct.demo.mapper;

import com.zheng.mapstruct.demo.data.Source;
import com.zheng.mapstruct.demo.data.Target;
import com.zheng.mapstruct.demo.service.ExternalService;
import com.zheng.mapstruct.demo.service.NameContext;
import javax.annotation.processing.Generated;
import org.springframework.stereotype.Component;

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2025-05-08T16:21:59-0500",
    comments = "version: 1.5.5.Final, compiler: IncrementalProcessingEnvironment from gradle-language-java-8.13.jar, environment: Java 17 (Oracle Corporation)"
)
@Component
public class MapperWithBeforeMappingImpl extends MapperWithBeforeMapping {

    @Override
    public Target map(Source user, ExternalService context, NameContext nameContext) {
        if ( user == null ) {
            return null;
        }

        Target target = new Target();

        setLookupValue( user, target, context );
        splitFullName( user, target, nameContext );

        target.setId( user.getId() );

        return target;
    }
}

5.4 MapperWithAfterMappingImpl

In this step, I will print the generated MapperWithAfterMappingImpl.java class.

MapperWithAfterMappingImpl.java

package com.zheng.mapstruct.demo.mapper;

import com.zheng.mapstruct.demo.data.Source;
import com.zheng.mapstruct.demo.data.Target;
import com.zheng.mapstruct.demo.service.ExternalService;
import com.zheng.mapstruct.demo.service.NameContext;
import javax.annotation.processing.Generated;
import org.springframework.stereotype.Component;

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2025-05-08T16:21:59-0500",
    comments = "version: 1.5.5.Final, compiler: IncrementalProcessingEnvironment from gradle-language-java-8.13.jar, environment: Java 17 (Oracle Corporation)"
)
@Component
public class MapperWithAfterMappingImpl extends MapperWithAfterMapping {

    @Override
    public Target map(Source user, ExternalService context, NameContext nameContext) {
        if ( user == null ) {
            return null;
        }

        Target target = new Target();

        target.setId( user.getId() );

        setLookupValue( user, target, context );
        splitFullName( user, target, nameContext );

        return target;
    }
}

5.5 MapperWithObjectFactoryImpl

In this step, I will print the generated MapperWithObjectFactoryImpl.java class.

MapperWithObjectFactoryImpl.java

package com.zheng.mapstruct.demo.mapper;

import com.zheng.mapstruct.demo.data.Source;
import com.zheng.mapstruct.demo.data.Target;
import com.zheng.mapstruct.demo.service.LookUpContext;
import com.zheng.mapstruct.demo.service.NameContext;
import javax.annotation.processing.Generated;
import org.springframework.stereotype.Component;

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2025-05-08T16:21:59-0500",
    comments = "version: 1.5.5.Final, compiler: IncrementalProcessingEnvironment from gradle-language-java-8.13.jar, environment: Java 17 (Oracle Corporation)"
)
@Component
public class MapperWithObjectFactoryImpl extends MapperWithObjectFactory {

    @Override
    public Target map(Source user, LookUpContext context, NameContext nameContext) {
        if ( user == null ) {
            return null;
        }

        Target target = createTarget( user, context, nameContext );

        return target;
    }
}

6. Test Context Annotation Complex Source Mappings

6.1 TestBase

In this step, I will create TestBase.java that defines the sourcetarget, externalService, lookUpContext, nameContext variables, and setupFields method.

TestBase.java

package com.zheng.mapstruct.demo.mapper;

import com.zheng.mapstruct.demo.data.Source;
import com.zheng.mapstruct.demo.data.Target;
import com.zheng.mapstruct.demo.service.ExternalService;
import com.zheng.mapstruct.demo.service.LookUpContext;
import com.zheng.mapstruct.demo.service.NameContext;

public class TestBase {

	protected Source source;

	protected Target target;

	protected static final String LOOK_UP_VALUE_1 = "lookUpValue_1";
	protected static final int ID1 = 1;
	protected static final String LAST_NAME = "LastName";
	protected static final String FIRST_NAME = "FirstName";
	protected static final String FULLNAME = "FirstName LastName";
	
	protected ExternalService externalService = new ExternalService();
	protected LookUpContext lookUpContext = new LookUpContext(externalService);

	protected NameContext nameContext = new NameContext();

	public TestBase() {
		super();
	}

	protected void setupFields(Integer someId, String fullName) {
		source = new Source();
		source.setId(someId);
		source.setFullName(fullName);
	}

}

6.2 MapperWithExpressionTest

In this step, I will create a MapperWithExpressionTest.java that verifies the target‘s lookUpValue field is set by the @context via expression and the firstName and lastName are set via @nameContext.

MapperWithExpressionTest.java

package com.zheng.mapstruct.demo.mapper;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;

import org.junit.jupiter.api.Test;

class MapperWithExpressionTest extends TestBase {

	
	private MapperWithExpression testClass = MapperWithExpression.INSTANCE;

	@Test
	void test_lookup_negative() {
		setupFields(-1, FULLNAME);

		assertThrows(RuntimeException.class, () -> testClass.map(source, externalService,nameContext));
	}

	@Test
	void test_lookup_null() {
		setupFields(null, FULLNAME);

		assertThrows(RuntimeException.class, () -> testClass.map(source, externalService,nameContext));
	}

	@Test
	void test_lookup_positive() {
		setupFields(ID1, FULLNAME);

		target = testClass.map(source, externalService,nameContext);

		assertEquals(ID1, target.getId().intValue());
		assertEquals(LOOK_UP_VALUE_1, target.getLookUpValue());	
		assertEquals(FIRST_NAME, target.getFirstName());
		assertEquals(LAST_NAME, target.getLastName());

	}
	
	@Test
	void test_source_null() {
		assertNull(testClass.map(source, externalService,nameContext));
	}

}

6.3 MapperWithQualifiedByNameTest

In this step, I will create a MapperWithQualifiedByNameTest.java that verifies the target‘s lookUpValue field is set by the @context via qualifiedByName and the firstName and lastName are set via @nameContext.

MapperWithQualifiedByNameTest.java

package com.zheng.mapstruct.demo.mapper;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;

import org.junit.jupiter.api.Test;

class MapperWithQualifiedByNameTest extends TestBase {

	private MapperWithQualifiedByName testClass = MapperWithQualifiedByName.INSTANCE;

	@Test
	void test_lookup_negative() {
		setupFields(-1, FULLNAME);

		assertThrows(RuntimeException.class, () -> testClass.map(source, externalService));
	}

	@Test
	void test_lookup_null() {
		setupFields(null, FULLNAME);

		target =  testClass.map(source, externalService);
		assertNull(target.getId());
	}

	@Test
	void test_lookup_positive() {
		setupFields(ID1, FULLNAME);

		target = testClass.map(source, externalService);

		assertEquals(ID1, target.getId().intValue());
		assertEquals(LOOK_UP_VALUE_1, target.getLookUpValue());
		assertEquals(FIRST_NAME, target.getFirstName());
		assertEquals(LAST_NAME, target.getLastName());

	}
	
	@Test
	void test_source_null() {
		assertNull(testClass.map(source, externalService));
	}

}

6.4 MapperWithBeforeMappingTest

In this step, I will create a MapperWithQualifiedByNameTest.java that verifies the target‘s lookUpValue field is set by the @context and the firstName and lastName are set via @nameContext.

MapperWithBeforeMappingTest.java

package com.zheng.mapstruct.demo.mapper;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;

import org.junit.jupiter.api.Test;

class MapperWithBeforeMappingTest extends TestBase {

	
	private MapperWithBeforeMapping testClass = MapperWithBeforeMapping.INSTANCE;

	@Test
	void test_lookup_negative() {
		setupFields(-1, FULLNAME);

		assertThrows(RuntimeException.class, () -> testClass.map(source, externalService, nameContext));
	}

	@Test
	void test_lookup_null() {
		setupFields(null, FULLNAME);

		assertThrows(RuntimeException.class, () -> testClass.map(source, externalService, nameContext));
	}

	@Test
	void test_lookup_positive() {
		setupFields(ID1, FULLNAME );

		target = testClass.map(source, externalService, nameContext);

		assertEquals(ID1, target.getId().intValue());
		assertEquals(LOOK_UP_VALUE_1, target.getLookUpValue());
		assertEquals(FIRST_NAME, target.getFirstName());
		assertEquals(LAST_NAME, target.getLastName());
	}

	@Test
	void test_source_null() {
		assertNull(testClass.map(source, externalService, nameContext));
	}

}

6.5 MapperWithAfterMappingTest

In this step, I will create a MapperWithQualifiedByNameTest.java that verifies the target‘s lookUpValue field is set by the @context and the firstName and lastName are set via @nameContext.

MapperWithAfterMappingTest.java

package com.zheng.mapstruct.demo.mapper;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;

import org.junit.jupiter.api.Test;

class MapperWithAfterMappingTest extends TestBase {

	private MapperWithAfterMapping testClass = MapperWithAfterMapping.INSTANCE;

	@Test
	void test_lookup_negative() {
		setupFields(-1, FULLNAME);

		assertThrows(RuntimeException.class, () -> testClass.map(source, externalService,nameContext));
	}

	@Test
	void test_lookup_null() {
		setupFields(null, FULLNAME);

		assertThrows(RuntimeException.class, () -> testClass.map(source, externalService,nameContext));
	}

	@Test
	void test_lookup_positive() {
		setupFields(ID1, FULLNAME);

		target = testClass.map(source, externalService,nameContext);

		assertEquals(ID1, target.getId().intValue());
		assertEquals(LOOK_UP_VALUE_1, target.getLookUpValue());
		assertEquals(FIRST_NAME, target.getFirstName());
		assertEquals(LAST_NAME, target.getLastName());

	}

	@Test
	void test_source_null() {

		assertNull(testClass.map(source, externalService,nameContext));
	}
}

6.6 MapperWithObjectFactoryTest

In this step, I will create a MapperWithQualifiedByNameTest.java that verifies the target‘s lookUpValue field is set by the @context and the firstName and lastName are set via @nameContext.

MapperWithObjectFactoryTest.java

package com.zheng.mapstruct.demo.mapper;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;

import org.junit.jupiter.api.Test;

class MapperWithObjectFactoryTest extends TestBase {

	private MapperWithObjectFactory testClass = MapperWithObjectFactory.INSTANCE;

	@Test
	void test_lookup_negative() {
		setupFields(-1, FULLNAME);

		assertThrows(RuntimeException.class, () -> testClass.map(source, lookUpContext,nameContext));
	}

	@Test
	void test_lookup_null() {
		setupFields(null, FULLNAME);

		assertThrows(RuntimeException.class, () -> testClass.map(source, lookUpContext,nameContext));
	}

	@Test
	void test_lookup_positive() {
		setupFields(ID1, FULLNAME);

		target = testClass.map(source, lookUpContext,nameContext);

		assertEquals(ID1, target.getId().intValue());
		assertEquals(LOOK_UP_VALUE_1, target.getLookUpValue());
		assertEquals(FIRST_NAME, target.getFirstName());
		assertEquals(LAST_NAME, target.getLastName());

	}

	@Test
	void test_source_null() {
		assertNull(testClass.map(source, lookUpContext,nameContext));
	}

}

Run the tests and capture the results in Figure 1.

@context complex source mappings
Figure 1 Test Results

7. Conclusion

In this example, I demonstrated context annotation complex source mappings by creating Mapper Interfaces and Abstract Mapper Classes with @Context, @AfterMapping, @BeforeMapping, @ObjectFactory, Expression, and QualifiedByName. I outlined the difference in the following table.

@AfterMapping@BeforeMapping@ObjectFactoryExpressionQualifiedByName
DescriptionMarks a method to be invoked at the end of a generated mapping method, right before the last return statement of the mapping method. Marks a method to be invoked at the beginning of a generated mapping method. Marks a method as a factory method to create beans.An expression String based on which the specified target property is to be set.String-based form of qualifiers
Why Interface?Simple MappingDefault method
Why Abstract Class?Custom Mapping and Injecting the dependenciesCustom Mapping and Injecting the dependenciesCustom Mapping and Injecting the dependencies
Why ExternalService?Only change the mapped fieldOnly change the mapped fieldLookUpContextOnly change the mapped fieldOnly change the mapped field
Why LookUpContext?Use the target created via the ObjectFactory

8. Download

This was an example of a Gradle project which included mapstruct context annotation complex source mappings example.

Download
You can download the full source code of this example here: @Context Complex Source Mappings Example

Mary Zheng

Mary graduated from the Mechanical Engineering department at ShangHai JiaoTong University. She also holds a Master degree in Computer Science from Webster University. During her studies she has been involved with a large number of projects ranging from programming and software engineering. She worked as a lead Software Engineer where she led and worked with others to design, implement, and monitor the software solution.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button