Running Preconfigured RabbitMQ Server Using Docker In Windows

Create Two folders, data and etc

enabled_plugins

[rabbitmq_management,rabbitmq_prometheus].

rabbitmq.conf

auth_mechanisms.1 = PLAIN
auth_mechanisms.2 = AMQPLAIN
loopback_users.guest = false
listeners.tcp.default = 5672
#default_pass = admin
#default_user = admin
hipe_compile = false
#management.listener.port = 15672
#management.listener.ssl = false
management.tcp.port = 15672
management.load_definitions = /etc/rabbitmq/definitions.json
#default_pass = admin
#default_user = admin

definitions.json

{
   "users": [
    {
      "name": "admin",
      "password": "admin",
      "tags": "administrator"
    }
  ],
  "vhosts": [
    {
      "name": "/"
    }
  ],
  "policies": [
    {
      "vhost": "/",
      "name": "ha",
      "pattern": "",
      "apply-to": "all",
      "definition": {
        "ha-mode": "all",
        "ha-sync-batch-size": 256,
        "ha-sync-mode": "automatic"
      },
      "priority": 0
    }
  ],
  "permissions": [
    {
      "user": "admin",
      "vhost": "/",
      "configure": ".*",
      "write": ".*",
      "read": ".*"
    }
  ],
  "queues": [
    {
      "name": "job-import.triggered.queue",
      "vhost": "/",
      "durable": true,
      "auto_delete": false,
      "arguments": {}
    }
  ],
  "exchanges": [
    {
      "name": "lob-proj-dx",
      "vhost": "/",
      "type": "direct",
      "durable": true,
      "auto_delete": false,
      "internal": false,
      "arguments": {}
    }
  ],
  "bindings": [
    {
      "source": "lob-proj-dx",
      "vhost": "/",
      "destination": "job-import.triggered.queue",
      "destination_type": "queue",
      "routing_key": "job-import.event.triggered",
      "arguments": {}
    }
  ]
}

docker run --restart=always -d -p 5672:5672 -p 15672:15672 --mount type=bind,source=E:\docker\rabbit\data,target=/var/lib/rabbitmq/ --mount type=bind,source=E:\docker\rabbit\etc,target=/etc/rabbitmq/ --name rabbitmq --hostname my-rabbit rabbitmq:3.7.28-management
docker run --restart=always \
-d \
-p 5672:5672 \
-p 15672:15672  \
--mount type=bind,source=E:\docker\rabbit\data,target=/var/lib/rabbitmq/ \
--mount type=bind,source=E:\docker\rabbit\etc,target=/etc/rabbitmq/ \
--name rabbitmq \
--hostname my-rabbit \
rabbitmq:3.7.28-management

Things are persisted across restarts

Running MySQL

docker run --name my-mysql -e MYSQL_ROOT_PASSWORD=Admin@123 -d -p 3306:3306 mysql

Spring Boot Data JPA Multiple Data Sources

aplication.properties

# Data sources
abc.datasource.url=jdbc:mysql://${GFR_DB_HOST}:${GFR_DB_PORT}/abc
abc.datasource.driverClassName=com.mysql.cj.jdbc.Driver
abc.datasource.username=${GFR_DB_USER}
abc.datasource.password=${GFR_DB_PASS}

# Hibernate props
abc.hibernate.hbm2ddl.auto=update
abc.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
abc.hibernate.show_sql=true

### Connection Pool Details
#abc.datasource.hikari.connectionTimeout=20000
#abc.datasource.hikari.maximumPoolSize=5
#abc.datasource.hikari.connection-test-query=SELECT 1
#abc.datasource.hikari.minimum-idle=5 
#abc.datasource.hikari.maximum-pool-size=20
#abc.datasource.hikari.idle-timeout=600000 
#abc.datasource.hikari.max-lifetime=1800000 
#abc.datasource.hikari.auto-commit=true
#abc.datasource.hikari.poolName=SpringBoot-HikariCP

Repository Config

import java.util.HashMap;
import java.util.Map;

import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.data.envers.repository.config.EnableEnversRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.context.annotation.Primary;

import com.zaxxer.hikari.HikariDataSource;

@Configuration
@EnableEnversRepositories(basePackages = {"com.org.lob.abc.membrer.repository"},
		entityManagerFactoryRef = "abcEntityManagerFactory",
		transactionManagerRef = "abcTransactionManager")
public class ABCRepositoryConfig {
	@Autowired
	private Environment env;

    @Primary
	@Bean(name = "abcDataSourceProperties")
	@ConfigurationProperties("abc.datasource")
	DataSourceProperties dataSourceProperties() {
		return new DataSourceProperties();
	}
	
    @Primary
	@Bean(name = "abcDataSource")
	DataSource dataSource(@Qualifier("abcDataSourceProperties") DataSourceProperties dataSourceProperties) {
		return dataSourceProperties
				.initializeDataSourceBuilder()
				.type(HikariDataSource.class)
				.build();
	}
	
    @Primary
	@Bean(name = "abcEntityManagerFactory")
	LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder builder, @Qualifier("abcDataSource") DataSource dataSource) {
		return builder
				.dataSource(dataSource)
				.packages("com.org.lob.abc.membrer.repository")
				.persistenceUnit("abc")
				.properties(abcJpaProperties())
				.build();
	}

	private Map<String, String> abcJpaProperties() {
		Map<String, String> jpaProperties = new HashMap<>();

		jpaProperties.put("hibernate.dialect", env.getProperty("abc.hibernate.dialect"));
		jpaProperties.put("hibernate.hbm2ddl.auto", env.getProperty("abc.hibernate.hbm2ddl.auto"));
		jpaProperties.put("hibernate.show_sql", env.getProperty("abc.hibernate.show_sql"));

		return jpaProperties;
	}
	
    @Primary
	@Bean(name = "abcTransactionManager")
	PlatformTransactionManager transactionManager(@Qualifier("abcEntityManagerFactory") EntityManagerFactory entityManagerFactory) {
		return new JpaTransactionManager(entityManagerFactory);
	}

}

You can add another config

# Data sources
pbm.datasource.url=jdbc:mysql://${GFR_DB_HOST}:${GFR_DB_PORT}/${GFR_DB_NAME}
pbm.datasource.driverClassName=com.mysql.cj.jdbc.Driver
pbm.datasource.username=${GFR_DB_USER}
pbm.datasource.password=${GFR_DB_PASS}

# Hibernate props
pbm.hibernate.hbm2ddl.auto=update
pbm.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
pbm.hibernate.show_sql=true

Repository Config

import java.util.HashMap;
import java.util.Map;

import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.env.Environment;
import org.springframework.data.domain.AuditorAware;
import org.springframework.data.envers.repository.config.EnableEnversRepositories;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;

import com.zaxxer.hikari.HikariDataSource;

@Configuration
@EnableJpaAuditing
@EnableEnversRepositories(basePackages = {"com.org.lob.pbm.member.repository"},
		entityManagerFactoryRef = "pbmEntityManagerFactory",
		transactionManagerRef = "pbmTransactionManager")
public class PBMRepositoryConfig {

	@Autowired
	private Environment env;

	
	@Bean(name = "pbmDataSourceProperties")
	@ConfigurationProperties("pbm.datasource")
	DataSourceProperties dataSourceProperties() {
		return new DataSourceProperties();
	}
	
	
	@Bean(name = "pbmDataSource")
	DataSource dataSource(@Qualifier("pbmDataSourceProperties") DataSourceProperties dataSourceProperties) {
		return dataSourceProperties
				.initializeDataSourceBuilder()
				.type(HikariDataSource.class)
				.build();
	}
	
	
	@Bean(name = "pbmEntityManagerFactory")
	LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder builder, @Qualifier("pbmDataSource") DataSource dataSource) {
		return builder
				.dataSource(dataSource)
				.packages("com.org.lob.pbm.member.repository")
				.persistenceUnit("pbm")
				.properties(pbmJpaProperties())
				.build();
	}

	private Map<String, String> pbmJpaProperties() {
		Map<String, String> jpaProperties = new HashMap<>();

		jpaProperties.put("hibernate.dialect", env.getProperty("pbm.hibernate.dialect"));
		jpaProperties.put("hibernate.hbm2ddl.auto", env.getProperty("pbm.hibernate.hbm2ddl.auto"));
		jpaProperties.put("hibernate.show_sql", env.getProperty("pbm.hibernate.show_sql"));
		//jpaProperties.put("hibernate.naming.physical-strategy", env.getProperty("org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy"));

		return jpaProperties;
	}
	
	
	@Bean(name = "pbmTransactionManager")
	PlatformTransactionManager transactionManager(@Qualifier("pbmEntityManagerFactory") EntityManagerFactory entityManagerFactory) {
		return new JpaTransactionManager(entityManagerFactory);
	}

	@Bean
	AuditorAware<String> auditorProvider() {
		return new RequestAttributeAuditorAware();
	}
}

Spring Boot With H2 DataBase

https://start.spring.io/

In Memory

spring.datasource.url=jdbc:h2:mem:testdb
#spring.datasource.url=jdbc:h2:file:~/test
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect

# Enabling H2 Console
spring.h2.console.enabled=true
# Custom H2 Console UR
#spring.h2.console.path=/h2-console
# Whether to enable trace output.
#spring.h2.console.settings.trace=false
# Whether to enable remote access.
#spring.h2.console.settings.web-allow-others=false

http://localhost:8080/h2-console

File DB

#spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.url=jdbc:h2:file:~/test;DB_CLOSE_ON_EXIT=FALSE;AUTO_SERVER=TRUE
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect

Connect with DBeaver

Edit Driver Settings

Other related properties

spring.jpa.show-sql=true
spring.jpa.generate-ddl=true
spring.jpa.hibernate.ddl-auto=update
#spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect
#spring.jpa.properties.hibernate.type=trace
#spring.jpa.properties.hibernate.show_sql=true
#spring.jpa.properties.hibernate.format_sql=true
#spring.jpa.properties.hibernate.use_sql_comments=false

## default connection pool
spring.datasource.hikari.connectionTimeout=20000
spring.datasource.hikari.maximumPoolSize=5
#spring.datasource.hikari.connection-test-query=SELECT 1
#spring.datasource.hikari.minimum-idle=5 
#spring.datasource.hikari.maximum-pool-size=20
#spring.datasource.hikari.idle-timeout=600000 
#spring.datasource.hikari.max-lifetime=1800000 
#spring.datasource.hikari.auto-commit=true
#spring.datasource.hikari.poolName=SpringBoot-HikariCP

Spring Boot Caching With Ehcache3

Dependencies

Add the following to your pom

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-cache</artifactId>
		</dependency>
		<dependency>
		    <groupId>org.ehcache</groupId>
		    <artifactId>ehcache</artifactId>
		</dependency>
		<dependency>
		    <groupId>javax.cache</groupId>
		    <artifactId>cache-api</artifactId>
		</dependency>

Cache configuration

Keep the following ehcache.xml under src/main/resources

<?xml version="1.0" encoding="UTF-8"?>
<config xmlns='http://www.ehcache.org/v3'>

	<persistence directory="${java.io.tmpdir}" />

	<!-- Default cache template -->
	<cache-template name="default">
		<expiry>
			<tti unit="hours">4</tti>
			<!-- <ttl unit="minutes">2</ttl> -->
		</expiry>
		<listeners>
			<listener>
				<class>com.org.lob.support.LoggingTaskCacheListener</class>
				<event-firing-mode>ASYNCHRONOUS</event-firing-mode>
				<event-ordering-mode>UNORDERED</event-ordering-mode>
				<events-to-fire-on>CREATED</events-to-fire-on>
				<events-to-fire-on>EXPIRED</events-to-fire-on>
				<events-to-fire-on>REMOVED</events-to-fire-on>
				<events-to-fire-on>UPDATED</events-to-fire-on>
			</listener>
		</listeners>
		<resources>
			<heap unit="MB">10</heap>
			<offheap unit="MB">50</offheap>
			<disk persistent="true" unit="GB">1</disk>
		</resources>
		<!-- 
		<heap-store-settings>
			<max-object-graph-size>2000</max-object-graph-size>
			<max-object-size unit="kB">5</max-object-size>
		</heap-store-settings>
		-->
	</cache-template>

	<!-- Cache configurations -->
	<cache alias="books" uses-template="default" >
		<key-type>java.lang.String</key-type>
		<value-type>com.org.lob.project.repository.entity.Book</value-type>		
	</cache>

	<cache alias="files" uses-template="default" >
		<key-type>java.lang.String</key-type>
		<value-type>java.lang.String</value-type>		
	</cache>

</config>

Spring Boot Config

Add the following to your application.properties

# Cache
spring.cache.jcache.config=classpath:ehcache.xml

Add the following spring boot config

import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableCaching
public class CacheConfig {
	
}

Start Caching

@Component
@CacheConfig(cacheNames = "books")
public class SimpleBookRepository implements BookRepository {
	
	@Cacheable
	@Override
	public Book getByIsbn(String isbn) {
		simulateSlowService();
		return new Book(isbn, "Some book");
	}

	// Don't do this at home
	private void simulateSlowService() {
		try {
			long time = 3000L;
			Thread.sleep(time);
		} catch (InterruptedException e) {
			throw new IllegalStateException(e);
		}
	}
}

@Component
@CacheConfig(cacheNames = "files")
public class SimpleFileRepository implements FileRepository {

	private static final Logger LOGGER = LoggerFactory.getLogger(SimpleFileRepository.class);

	@Override
	@Cacheable
	public String load(String project) {
		return asString(new FileSystemResource(project));
	}

	@Override
	@CachePut
	public String reLoad(String project) {
		return asString(new FileSystemResource(project));
	}

	private String asString(Resource resource) {
        try (Reader reader = new InputStreamReader(resource.getInputStream(), UTF_8)) {
            return FileCopyUtils.copyToString(reader);
        } catch (IOException e) {
        	LOGGER.error("Error Proessing ", e);
            throw new UncheckedIOException(e);
        }
    }
}

Source Code

Download the source code from github

Add Performance Monitor Aspect To Spring Boot Application

Add Aspect

package com.lob.proj.config;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.interceptor.PerformanceMonitorInterceptor;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@EnableAspectJAutoProxy
@Aspect
public class AspectConfig {

	@Pointcut("execution(public * com.lob.proj.api.CustomerApi.*(..))")
	public void monitor() {
	}

	@Bean
	PerformanceMonitorInterceptor performanceMonitorInterceptor() {
		return new PerformanceMonitorInterceptor(false);
	}

	@Bean
	Advisor performanceMonitorAdvisor(PerformanceMonitorInterceptor performanceMonitorInterceptor) {
		AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
		pointcut.setExpression("com.lob.proj.config.AspectConfig.monitor()");
		return new DefaultPointcutAdvisor(pointcut, performanceMonitorInterceptor);
	}
}

Add Logging level

logging.level.org.springframework.aop.interceptor.PerformanceMonitorInterceptor=TRACE

MapStruct Mapping List To Individual Fields And Vice Versa

public class Lookup {

	private String name;
	private String description;

	private String param1;
	private String param2;
	private String param3;
	private String param4;

	public int paramsCount() {
		int result = 0;
		
		if (isNotEmpty(param4)) {
			result = 4;
		} else if (isNotEmpty(param3)) {
			result = 3;
		} else if (isNotEmpty(param2)) {
			result = 2;
		} else if (isNotEmpty(param1)) {
			result = 1;
		}		
		return result;
	}

	private boolean isNotEmpty(String param42) {
		return param42 != null && param42.trim().length() > 0;
	}

	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public String getDescription() {
		return description;
	}
	public void setDescription(String description) {
		this.description = description;
	}
	public String getParam1() {
		return param1;
	}
	public void setParam1(String param1) {
		this.param1 = param1;
	}
	public String getParam2() {
		return param2;
	}
	public void setParam2(String param2) {
		this.param2 = param2;
	}
	public String getParam3() {
		return param3;
	}
	public void setParam3(String param3) {
		this.param3 = param3;
	}
	public String getParam4() {
		return param4;
	}
	public void setParam4(String param4) {
		this.param4 = param4;
	}

	@Override
	public String toString() {
		return "Lookup [name=" + name + ", description=" + description + ", param1=" + param1 + ", param2=" + param2
				+ ", param3=" + param3 + ", param4=" + param4 + "]";
	}
}

import java.util.List;

public class LookupModel {
	
	private String name;
	private String description;
	private List<String> params;

	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public String getDescription() {
		return description;
	}
	public void setDescription(String description) {
		this.description = description;
	}
	public List<String> getParams() {
		return params;
	}
	public void setParams(List<String> params) {
		this.params = params;
	}
	
	@Override
	public String toString() {
		return "LookupModel [name=" + name + ", description=" + description + ", params=" + params + "]";
	}
}

Approach #1

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

@Mapper
public interface LookupMapper {
	
	LookupMapper INSTANCE = Mappers.getMapper( LookupMapper.class );

	@Mapping(source = "source", target = "param1", qualifiedByName = "param1")
	@Mapping(source = "source", target = "param2", qualifiedByName = "param2")
	@Mapping(source = "source", target = "param3", qualifiedByName = "param3")
	@Mapping(source = "source", target = "param4", qualifiedByName = "param4")
	Lookup toLookup(LookupModel source);

	@Named("param1")
    default String lookupModelToParam1(LookupModel source) {
		return getParam(0, source);
    }

	@Named("param2")
    default String lookupModelToParam2(LookupModel source) {
		return getParam(1, source);
    }

	@Named("param3")
    default String lookupModelToParam3(LookupModel source) {
       return getParam(2, source);
    }

	@Named("param4")
    default String lookupModelToParam4(LookupModel source) {
       return getParam(3, source);
    }

	default String getParam(int index, LookupModel source) {
		if (source.getParams().size() > index) {
			return source.getParams().get(index);
		}
		return null;
	}

	@Mapping(source = "source", target = "params", qualifiedByName = "params")
	LookupModel toLookupModel(Lookup source);

	@Named("params")
	default List<String> lookupToParams(Lookup source) {
		return extractParams(source);
	}

	default List<String> extractParams(Lookup source) {
		List<String> result = new ArrayList<String>();

		int paramsCount = source.paramsCount();
		
		if (paramsCount > 0) {
			result.add(source.getParam1());
		}
		
		if (paramsCount > 1) {
			result.add(source.getParam2());
		}
		
		if (paramsCount > 2) {
			result.add(source.getParam3());
		}
		
		if (paramsCount > 3) {
			result.add(source.getParam4());
		}
		return result;
	}
}

Approach #2

@Mapper
public interface LookupMapper {
	
	LookupMapper INSTANCE = Mappers.getMapper( LookupMapper.class );

	Lookup toLookup(LookupModel source);

	@AfterMapping
	default void populateAdditionalParams(LookupModel source, @MappingTarget Lookup target) {

		int idx = 0;
		for (String param : source.getParams()) {
			idx++;
			if (idx == 1) {
				target.setParam1(param);
			} else if (idx == 2) {
				target.setParam2(param);
			}else if (idx == 3) {
				target.setParam3(param);
			}else if (idx == 4) {
				target.setParam4(param);
			}
		}
	}

	LookupModel toLookupModel(Lookup source);

	@AfterMapping
	default void populateAdditionalParams(Lookup source, @MappingTarget LookupModel target) {
		target.setParams(extractParams(source));
	}

	default List<String> extractParams(Lookup source) {
		List<String> result = new ArrayList<String>();

		int paramsCount = source.paramsCount();
		
		if (paramsCount > 0) {
			result.add(source.getParam1());
		}
		
		if (paramsCount > 1) {
			result.add(source.getParam2());
		}
		
		if (paramsCount > 2) {
			result.add(source.getParam3());
		}
		
		if (paramsCount > 3) {
			result.add(source.getParam4());
		}
		return result;
	}
}

Mapping Between JPA Entities And DTOs In Spring Boot Application

Source Code can be found here

Update Pom

Update your build.plugins

<plugin>
	            <groupId>org.apache.maven.plugins</groupId>
	            <artifactId>maven-compiler-plugin</artifactId>
	            <configuration>
	                <source>${java.version}</source>
	                <target>${java.version}</target>
	                <annotationProcessorPaths>
	                    <path>
	                        <groupId>org.mapstruct</groupId>
	                        <artifactId>mapstruct-processor</artifactId>
	                        <version>${mapstruct.version}</version>
	                    </path>
	                </annotationProcessorPaths>
	            </configuration>
	        </plugin>

Add mapstruct dependencies

		<dependency>
		    <groupId>org.mapstruct</groupId>
		    <artifactId>mapstruct</artifactId>
		    <version>${mapstruct.version}</version>
		</dependency>

Structure

Create DTOs (Models)

We have created the Models AddressModel and CustomerModel

Create Mapper

import java.util.List;

import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;

import com.org.lob.project.repository.entity.Address;
import com.org.lob.project.service.model.AddressModel;

@Mapper(componentModel = "spring")
public interface AddressMapper {

	AddressMapper INSTANCE = Mappers.getMapper(AddressMapper.class);

	List<AddressModel> toAddressModels(List<Address> source);

	List<Address> toAddresses(List<AddressModel> source);
}

import java.util.List;

import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;

import com.org.lob.project.repository.entity.Customer;
import com.org.lob.project.service.model.CustomerModel;

@Mapper(componentModel = "spring", uses = AddressMapper.class)
public interface CustomerMapper {

	CustomerMapper INSTANCE = Mappers.getMapper(CustomerMapper.class);

	Customer toCustomer(CustomerModel source);

	CustomerModel toCustomerModel(Customer source);

	List<CustomerModel> toCustomerModels(List<Customer> source);
}

Generate sources

Execute the following before usage

mvn clean package

which would generate the sources as follows

you can add the generated-sources folder as source folder

Use Mapper in Service

import java.util.List;
import java.util.Optional;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

import com.org.lob.project.exception.ProjectException;
import com.org.lob.project.repository.CustomerRepository;
import com.org.lob.project.repository.entity.Customer;
import com.org.lob.project.service.mapper.AddressMapper;
import com.org.lob.project.service.mapper.CustomerMapper;
import com.org.lob.project.service.model.CustomerModel;
import com.org.lob.project.service.model.CustomerSearchRequest;
import com.org.lob.project.service.specification.CustomerSpecification;

@Service
public class DefaultCustomerService implements CustomerService {

	private static final Logger LOGGER = LoggerFactory.getLogger(DefaultCustomerService.class);

	private final CustomerRepository customerRepository;
	private final CustomerMapper customerMapper;
	private final AddressMapper addressMapper;

	public DefaultCustomerService(CustomerRepository customerRepository, CustomerMapper customerMapper, AddressMapper addressMapper) {
		this.customerRepository = customerRepository;
		this.customerMapper = customerMapper;
		this.addressMapper = addressMapper;
	}

	@Override
	public Optional<CustomerModel> getCustomerById(Long customerId) {
		LOGGER.debug("Fetching customer by id: {}", customerId);
		Optional<Customer> optionalCustomer = customerRepository.findById(customerId);
		return optionalCustomer.isPresent() ? Optional.of(customerMapper.toCustomerModel(optionalCustomer.get())) : Optional.empty();
	}

	@Override
	public CustomerModel create(CustomerModel customerModel) {
		try {
			LOGGER.debug("Creating a new customer with emailAddress: {}", customerModel.getEmailAddress());
			return customerMapper.toCustomerModel(customerRepository.save(customerMapper.toCustomer(customerModel)));
		} catch (DataIntegrityViolationException e) {
			LOGGER.error("Customer already exists with emailAddress: {}", customerModel.getEmailAddress());
			throw ProjectException.duplicateRecord("Customer already exists with same emailAddress " + customerModel.getEmailAddress());
		}
	}

	@Override
	public CustomerModel update(CustomerModel customerModel) {
		LOGGER.debug("Updating a customer with id: {}", customerModel.getId());
		Optional<Customer> optionalCustomer = customerRepository.findById(customerModel.getId());
		if (!optionalCustomer.isPresent()) {
			LOGGER.error("Unable to update customer by id {}", customerModel.getId());
			throw ProjectException.noRecordFound("Customer does not exists " + customerModel.getId());
		}
		Customer existingCustomer = optionalCustomer.get();
		existingCustomer.setAddresses(addressMapper.toAddresses(customerModel.getAddresses()));
		existingCustomer.setFirstName(customerModel.getFirstName());
		existingCustomer.setLastName(customerModel.getLastName());
		return customerMapper.toCustomerModel(customerRepository.save(existingCustomer));
	}
	
	@Override
	public List<CustomerModel> findByName(String name) {
		return customerMapper.toCustomerModels(customerRepository.findAllByFirstNameContainingOrLastNameContaining(name, name));
	}

	@Override
	public Optional<CustomerModel> findByEmail(String email) {
		Optional<Customer> optionalCustomer = customerRepository.findCustomerByEmailAddress(email);
		return optionalCustomer.isPresent() ? Optional.of(customerMapper.toCustomerModel(optionalCustomer.get())) : Optional.empty();
	}

	// Paging implementation of findAll
	@Override
	public Page<CustomerModel> findAll(Pageable pageable) {
		return new PageImpl<>( customerMapper.toCustomerModels(customerRepository.findAll(pageable).getContent()));
	}

	@Override
	public void deleteCustomer(Long customerId) {
		try {
			customerRepository.deleteById(customerId);
		} catch (EmptyResultDataAccessException e) {
			LOGGER.error("Unable to delete customer by id {}", customerId);
			throw ProjectException.noRecordFound("Customer does not exists " + customerId);
		}
	}

	@Override
	public Page<CustomerModel> search(CustomerSearchRequest request, Pageable pageable) {
		return new PageImpl<>(customerMapper.toCustomerModels(customerRepository.findAll(new CustomerSpecification(request), pageable).getContent()));
	}

	@Override
	public List<CustomerModel> findAllById(Iterable<Long> ids) {
		return customerMapper.toCustomerModels(customerRepository.findAllById(ids));
	}
}

References

Inter-service Communication Using Self-Signed JWT

Source Code can be found here

Project Structure

<dependency>
		    <groupId>io.jsonwebtoken</groupId>
		    <artifactId>jjwt</artifactId>
		    <version>0.9.1</version>
		</dependency>

JwtToken.java

import java.util.Date;

public class JwtToken {
	
	private Date iat;
	private Date expiration;
	private String token;

	public JwtToken(Date iat, Date expiration, String token) {
		this.iat = iat;
		this.expiration = expiration;
		this.token = token;
	}

	public Date getIat() {
		return iat;
	}

	public Date getExpiration() {
		return expiration;
	}

	public String getToken() {
		return token;
	}

	public boolean isExpired() {
		return expiration.before(new Date());
	}

    public static Builder builder() {
        return new Builder();
    }

	public static class Builder {
		
		private Date iat;
		private Date expiration;
		private String token;
		
		public Builder issuedAt(Date iat) {
			this.iat = iat;
			return this;
		}
		
		public Builder expiration(Date expiration) {
			this.expiration = expiration;
			return this;
		}
		
		public Builder expiration(String token) {
			this.token = token;
			return this;
		}

		public JwtToken build() {
			return new JwtToken(iat, expiration, token);
		}
	}
}

DefaultJwtTokenService.java

import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import org.springframework.beans.factory.annotation.Value;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

public class DefaultJwtTokenService implements JwtTokenService {

	private static final String ROLE_SYSTEM = "SYSTEM";
	private static final String CLAIM_ROLES = "roles";

	@Value("${app.jwt.secret}")
	private String jwtSecret;

	@Value("${app.jwt.token_duration.minutes}")
	private long tokenDurationInMinutes;

	@Override
	public JwtToken generateToken(String user) {
		Map<String, Object> claims = new HashMap<>();
		return doGenerateToken(claims, user);
	}

	// while creating the token -
	// 1. Define claims of the token, like Issuer, Expiration, Subject, and the ID
	// 2. Sign the JWT using the HS512 algorithm and secret key.
	// 3. According to JWS Compact
	// Serialization(https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-41#section-3.1)
	// compaction of the JWT to a URL-safe string
	private JwtToken doGenerateToken(Map<String, Object> claims, String subject) {
		long currentTime = System.currentTimeMillis();
		long expiration = currentTime + TimeUnit.MINUTES.toMillis(tokenDurationInMinutes);
		Date iat = new Date(currentTime);
		Date exp = new Date(expiration);

		String token = Jwts.builder()
				.setClaims(claims)
				.setSubject(subject)
				.setIssuedAt(iat)
				.setExpiration(exp)
				.claim(CLAIM_ROLES, Arrays.asList(ROLE_SYSTEM))
				.signWith(SignatureAlgorithm.HS512, jwtSecret.getBytes())
				.compact();

		return new JwtToken(iat, exp, token);
	}
}

DefaultJwtTokenProvider.java

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class DefaultJwtTokenProvider implements JwtTokenProvider {

	private static final Logger LOGGER = LoggerFactory.getLogger(DefaultJwtTokenProvider.class);

	private static final String USER_NAME_PROJECT = "PROJECT";

	private final JwtTokenService jwtTokenService;
	private JwtToken jwtToken;

	public DefaultJwtTokenProvider(JwtTokenService jwtTokenService) {
		this.jwtTokenService = jwtTokenService;
		this.jwtToken = jwtTokenService.generateToken(USER_NAME_PROJECT);
	}

	@Override
	public String getJwtToken() {
		if (this.jwtToken.isExpired()) {
			refreshToken();
		}

		return jwtToken.getToken();
	}

	private void refreshToken() {
		LOGGER.debug("Token Experied, Refreshing token");
		this.jwtToken = jwtTokenService.generateToken(USER_NAME_PROJECT);
	}
}

BearerJWTTokenAuthInterceptor.java

import java.io.IOException;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;

public class BearerJWTTokenAuthInterceptor implements ClientHttpRequestInterceptor {

	private JwtTokenProvider jwtTokenProvider;

	public BearerJWTTokenAuthInterceptor(JwtTokenProvider jwtTokenProvider) {
		this.jwtTokenProvider = jwtTokenProvider;
	}

	@Override
	public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
		HttpHeaders headers = request.getHeaders();
		if (!headers.containsKey(HttpHeaders.AUTHORIZATION)) {
			headers.setBearerAuth(this.jwtTokenProvider.getJwtToken());
		}
		return execution.execute(request, body);
	}
}

JwtConfig.java

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.org.lob.support.security.jwt.BearerJWTTokenAuthInterceptor;
import com.org.lob.support.security.jwt.DefaultJwtTokenProvider;
import com.org.lob.support.security.jwt.DefaultJwtTokenService;
import com.org.lob.support.security.jwt.JwtTokenProvider;
import com.org.lob.support.security.jwt.JwtTokenService;

@Configuration
public class JwtConfig {

	@Bean
	JwtTokenService jwtTokenService() {
		return new DefaultJwtTokenService();
	}

	@Bean
	JwtTokenProvider jwtTokenProvider(JwtTokenService jwtTokenService) {
		return new DefaultJwtTokenProvider(jwtTokenService);
	}

	@Bean
	BearerJWTTokenAuthInterceptor bearerJWTTokenAuthInterceptor(JwtTokenProvider jwtTokenProvider) {
		return new BearerJWTTokenAuthInterceptor(jwtTokenProvider);
	}
}

RestClientConfig.java

import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

import com.org.lob.support.security.jwt.BearerJWTTokenAuthInterceptor;

@Configuration
public class RestClientConfig {

	@Bean(name = "otherSystemRestTemplate")
	RestTemplate restTemplate(RestTemplateBuilder builder, BearerJWTTokenAuthInterceptor bearerJWTTokenAuthInterceptor) {
		return builder.additionalInterceptors(bearerJWTTokenAuthInterceptor).build();
	}
}

OtherSystemJwtRestClient.java

import java.util.Arrays;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

import com.org.lob.othersystem.model.OtherSystem;

@Component
public class OtherSystemJwtRestClient {

	private static final Logger LOGGER = LoggerFactory.getLogger(OtherSystemJwtRestClient.class);

	private RestTemplate otherSystemRestTemplate;

	@Value("${app.other_service.url}")
	private String otherServiceUrl;

	public OtherSystemJwtRestClient(@Qualifier("otherSystemRestTemplate") RestTemplate restTemplate) {
		this.otherSystemRestTemplate = restTemplate;
	}

	public List<OtherSystem> getOtherSystems(String param) {
		LOGGER.info("Getting OtherSystems from {}", otherServiceUrl);

		ResponseEntity<OtherSystem[]> response = otherSystemRestTemplate.getForEntity(otherServiceUrl, OtherSystem[].class);

		return Arrays.asList(response.getBody());
	}

}

Securing Rest APIs With Spring Security And JWT

Source Code can be found here

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
		<dependency>
	      <groupId>org.springframework.boot</groupId>
	      <artifactId>spring-boot-starter-security</artifactId>
	    </dependency>

DefaultJwtTokenService.java

import static com.org.lob.support.Constants.PASSWORD_FAKE;
import static java.util.Optional.ofNullable;

import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;


public class DefaultJwtTokenService implements JwtTokenService {

	private static final String CLAIM_ROLES = "roles";
	//private static final String CLAIM_EMAIL = "email";

	@Value("${app.jwt.secret}")
	private String jwtSecret;

	@Value("${app.jwt.token_duration.minutes}")
	private long tokenDurationInMinutes;

	@Override
	public String getUsername(String token) {
		return getClaimFromToken(token, Claims::getSubject);
	}

	@Override
	public Date getExpirationDate(String token) {
		return getClaimFromToken(token, Claims::getExpiration);
	}

	@Override
	public User getUser(String token) {
		Claims claims = getAllClaimsFromToken(token);
		String userName = claims.getSubject();
		//String email = claims.get(CLAIM_EMAIL, String.class);
		@SuppressWarnings("unchecked")
		List<String> roles = (List<String>) claims.get(CLAIM_ROLES);
		return new User(userName, PASSWORD_FAKE, buildAuth(roles));
	}

	private Collection<? extends GrantedAuthority> buildAuth(List<String> roles) {		
		return ofNullable(roles).orElse(Collections.emptyList()).stream().map(role -> new SimpleGrantedAuthority(role)).collect(Collectors.toList());
	}

	public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
		Claims claims = getAllClaimsFromToken(token);
		return claimsResolver.apply(claims);
	}

	private Claims getAllClaimsFromToken(String token) {
		return Jwts.parser()
				.setSigningKey(jwtSecret.getBytes())
				.parseClaimsJws(token)
				.getBody();
	}

	@Override
	public String generateToken(UserDetails userDetails) {
		Map<String, Object> claims = new HashMap<>();
		return doGenerateToken(claims, userDetails.getUsername());
	}

	// while creating the token -
	// 1. Define claims of the token, like Issuer, Expiration, Subject, and the ID
	// 2. Sign the JWT using the HS512 algorithm and secret key.
	// 3. According to JWS Compact
	// Serialization(https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-41#section-3.1)
	// compaction of the JWT to a URL-safe string
	private String doGenerateToken(Map<String, Object> claims, String subject) {

		return Jwts.builder()
				.setClaims(claims)
				.setSubject(subject)
				.setIssuedAt(new Date(System.currentTimeMillis()))
				.setExpiration(new Date(System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(tokenDurationInMinutes)))
				.signWith(SignatureAlgorithm.HS512, jwtSecret.getBytes())
				.compact();
	}

	@Override
	public boolean validate(String token) {
		return !isTokenExpired(token);
	}

	private Boolean isTokenExpired(String token) {
		Date expiration = getExpirationDate(token);
		return expiration.before(new Date());
	}
}

JwtTokenFilter.java

import static com.org.lob.support.Constants.SPACE;
import static java.util.List.of;
import static java.util.Optional.ofNullable;
import static org.springframework.util.StringUtils.hasText;

import java.io.IOException;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;

public class JwtTokenFilter extends OncePerRequestFilter {

	private JwtTokenService JwtTokenService;

	public JwtTokenFilter(JwtTokenService jwtTokenService) {
		JwtTokenService = jwtTokenService;
	}

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		// Get authorization header and validate
        String header = request.getHeader(HttpHeaders.AUTHORIZATION);
        if (!hasText(header) || !header.startsWith("Bearer ")) {
        	filterChain.doFilter(request, response);
            return;
        }

        // Get jwt token and validate
        final String token = header.split(SPACE)[1].trim();
        if (!JwtTokenService.validate(token)) {
        	filterChain.doFilter(request, response);
            return;
        }

        // Get user identity and set it on the spring security context
        UserDetails userDetails = JwtTokenService.getUser(token);

        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                userDetails, null,
                ofNullable(userDetails).map(UserDetails::getAuthorities).orElse(of())
        );

        authentication
                .setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

        SecurityContextHolder.getContext().setAuthentication(authentication);
        filterChain.doFilter(request, response);		
	}
}

application.properties

# Security
spring.security.filter.order=10
app.jwt.secret=123456
app.jwt.token_duration.minutes=10

Update SpringSecurityAuditorAware.java

import static com.org.lob.support.Constants.SYSTEM_USER_DEFAULT;

import java.util.Optional;

import org.springframework.data.domain.AuditorAware;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;;

public class SpringSecurityAuditorAware implements AuditorAware<String> {

    @Override
    public Optional<String> getCurrentAuditor() {

    	Optional<String> secured =  Optional.ofNullable(SecurityContextHolder.getContext())
                 .map(SecurityContext::getAuthentication)
                 .filter(Authentication::isAuthenticated)
                 .map(Authentication::getPrincipal)
                 .map(User.class::cast)
                 .map(User::getUsername);

    	return secured.isEmpty() ? Optional.of(SYSTEM_USER_DEFAULT) : secured;
    }
}

JwtTokenConfig.java


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.org.lob.support.security.DefaultJwtTokenService;
import com.org.lob.support.security.JwtTokenFilter;
import com.org.lob.support.security.JwtTokenService;

@Configuration
public class JwtTokenConfig {

	@Bean
	JwtTokenService jwtTokenService() {
		return new DefaultJwtTokenService();
	}

	@Bean
	JwtTokenFilter jwtTokenFilter(JwtTokenService jwtTokenService) {
		return new JwtTokenFilter(jwtTokenService);
	}
}

SecurityConfig.java

import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

import com.org.lob.support.security.JwtTokenFilter;

@EnableWebSecurity
@EnableGlobalMethodSecurity(
        securedEnabled = true,
        jsr250Enabled = true,
        prePostEnabled = true
)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

	private static final Logger LOGGER = LoggerFactory.getLogger(SecurityConfig.class);

	private final JwtTokenFilter jwtTokenFilter;

	public SecurityConfig(JwtTokenFilter jwtTokenFilter) {
		this.jwtTokenFilter = jwtTokenFilter;
		// Inherit security context in async function calls
        SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
	}

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		 // Enable CORS and disable CSRF
        http = http.cors().and().csrf().disable();

        // Set session management to stateless
        http = http
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and();

        // Set unauthorized requests exception handler
        http = http
                .exceptionHandling()
                .authenticationEntryPoint(
                        (request, response, ex) -> {
                        	LOGGER.error("Unauthorized request - {}", ex.getMessage());
                            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, ex.getMessage());
                        }
                )
                .and();

        // Set permissions on endpoints
        http.authorizeRequests()
                .antMatchers("/").permitAll()
                .antMatchers("/actuator/**").permitAll()
                .antMatchers(HttpMethod.GET, "/status").permitAll()
                .antMatchers("/api/**").authenticated()
                .anyRequest().authenticated();

        // Add JWT token filter
        http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
	}

    // Used by spring security if CORS is enabled.
    @Bean
    CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.addAllowedOrigin("*");
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }
}

Testing

Does not works with wrong token or without token

Passing the right token

Get current time in millis from here

10 * 60 * 1000 = 600000 (10 minutes) ==> Token expiry

Add current time in millis to token expiry 1623041857629 + 600000 = 1623042457629 would go for exp

https://jwt.io/

{
  "sub": "mnadeem",
  "name": "Mohammad Nadeem",
  "iat": 1516239022,
  "exp": 1623042457629
}

Works

Also See

Exposing Spring Boot Rest API

Source Code of this project can be found here

Create Entities

Customer.java

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToMany;
import javax.validation.constraints.NotBlank;

import org.hibernate.envers.AuditOverride;
import org.hibernate.envers.Audited;
import org.hibernate.envers.NotAudited;

import com.fasterxml.jackson.annotation.JsonManagedReference;

@Entity(name = "PRJ_CUSTOMER")
@Audited
@AuditOverride(forClass = Auditable.class)
public class Customer extends Auditable implements Serializable {

	private static final long serialVersionUID = 1L;

	@Id
	@Column(name = "ID")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

	@Column(name = "FIRST_NAME")
    private String firstName;

	@Column(name = "LAST_NAME")
    private String lastName;

	@NotBlank(message = "{email.not_empty}")
	@Column(name = "EMAIL_ADDRESS")
    private String emailAddress;

    @OneToMany(fetch = FetchType.EAGER, orphanRemoval = true, targetEntity = Address.class, cascade = CascadeType.ALL)
    @JoinColumn(name = "CUSTOMER_ID")
    @JsonManagedReference
    @NotAudited
    private List<Address> addresses;

	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	public String getFirstName() {
		return firstName;
	}

	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}

	public String getLastName() {
		return lastName;
	}

	public void setLastName(String lastName) {
		this.lastName = lastName;
	}

	public String getEmailAddress() {
		return emailAddress;
	}

	public void setEmailAddress(String emailAddress) {
		this.emailAddress = emailAddress;
	}

	public List<Address> getAddresses() {
		return addresses;
	}

	public void setAddresses(List<Address> addresses) {
		if (this.addresses == null) {
			this.addresses = new ArrayList<>();
		} else {
			this.addresses.clear();
		}
		this.addresses.addAll(addresses);
	}

	@Override
	public String toString() {
		return "Customer [id=" + id + ", firstName=" + firstName + ", lastName=" + lastName + ", emailAddress="
				+ emailAddress + ", addresses=" + addresses + "]";
	}
}

Address.java

import java.io.Serializable;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToOne;

import com.fasterxml.jackson.annotation.JsonBackReference;

@Entity(name = "PRJ_ADDRESS")
public class Address extends Auditable implements Serializable {

	private static final long serialVersionUID = 1L;

	@Id
	@Column(name = "ID")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
	
	@Column(name = "STREET_ADDRESS")
    private String streetAddress;
	@Column(name = "CITY")
    private String city;
	@Column(name = "STATE_CODE")
    private String stateCode;
	@Column(name = "COUNTRY")
    private  String country;
	@Column(name = "ZIP_CODE")
    private String zipCode;

    @ManyToOne
    @JsonBackReference
    private Customer customer;

	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	public String getStreetAddress() {
		return streetAddress;
	}

	public void setStreetAddress(String streetAddress) {
		this.streetAddress = streetAddress;
	}

	public String getCity() {
		return city;
	}

	public void setCity(String city) {
		this.city = city;
	}

	public String getStateCode() {
		return stateCode;
	}

	public void setStateCode(String stateCode) {
		this.stateCode = stateCode;
	}

	public String getCountry() {
		return country;
	}

	public void setCountry(String country) {
		this.country = country;
	}

	public String getZipCode() {
		return zipCode;
	}

	public void setZipCode(String zipCode) {
		this.zipCode = zipCode;
	}

	public Customer getCustomer() {
		return customer;
	}

	public void setCustomer(Customer customer) {
		this.customer = customer;
	}

	@Override
	public String toString() {
		return "Address [id=" + id + ", streetAddress=" + streetAddress + ", city=" + city + ", stateCode=" + stateCode
				+ ", country=" + country + ", zipCode=" + zipCode + ", customer=" + customer + "]";
	}
}

Repository

CustomerRepository.java

import java.util.List;
import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.history.RevisionRepository;
import org.springframework.stereotype.Repository;

import com.org.lob.project.repository.entity.Customer;

@Repository
public interface CustomerRepository extends JpaRepository<Customer, Long> {

    //Contains search on either firstname or lastname
    List<Customer> findAllByFirstNameContainingOrLastNameContaining(String firstName, String lastName);

    Optional<Customer> findCustomerByEmailAddress(String email);
}

Service

CustomerService.java

import java.util.List;
import java.util.Optional;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

import com.org.lob.project.repository.CustomerRepository;
import com.org.lob.project.repository.entity.Customer;

@Service
public class CustomerService {

	private static final Logger LOGGER = LoggerFactory.getLogger(CustomerService.class);

	private CustomerRepository customerRepository;

	public CustomerService(CustomerRepository customerRepository) {
		this.customerRepository = customerRepository;
	}

	public Optional<Customer> getCustomerById(Long customerId) {
		LOGGER.debug("Fetching customer by id: {}", customerId);
		return customerRepository.findById(customerId);
	}

	public Customer create(Customer customer) {
		try {
			LOGGER.debug("Creating a new customer with emailAddress: {}", customer.getEmailAddress());
			return customerRepository.save(customer);
		} catch (DataIntegrityViolationException e) {
			LOGGER.error("Customer already exists with emailAddress: {}", customer.getEmailAddress());
			throw new RuntimeException("Customer already exists with same emailAddress");
		}
	}

	public Customer update(Customer customer) {
		LOGGER.debug("Updating a customer with id: {}", customer.getId());
		Optional<Customer> optionalCustomer = customerRepository.findById(customer.getId());
		if (optionalCustomer.isEmpty()) {
			LOGGER.error("Unable to update customer by id {}", customer.getId());
			throw new RuntimeException("Customer does not exists");
		}
		Customer existingCustomer = optionalCustomer.get();
		existingCustomer.setAddresses(customer.getAddresses());
		existingCustomer.setFirstName(customer.getFirstName());
		existingCustomer.setLastName(customer.getLastName());
		return customerRepository.save(existingCustomer);
	}

	public List<Customer> findByName(String name) {
		return customerRepository.findAllByFirstNameContainingOrLastNameContaining(name, name);
	}

	public Optional<Customer> findByEmail(String email) {
		return customerRepository.findCustomerByEmailAddress(email);
	}

	// Paging implementation of findAll
	public Page<Customer> findAll(Pageable pageable) {
		return customerRepository.findAll(pageable);
	}

	public void deleteCustomer(Long customerId) {
		try {
			customerRepository.deleteById(customerId);
		} catch (EmptyResultDataAccessException e) {
			LOGGER.error("Unable to delete customer by id {}", customerId);
			throw new RuntimeException("Customer does not exists");
		}
	}
}

API

CustomerApi.java

import static com.org.lob.support.Constants.PATH_VARIABLE_ID;
import static com.org.lob.support.Constants.REQUEST_MAPPING_CUSTOMER;
import static com.org.lob.support.Constants.REQUEST_PARAM_PAGE_NUMBER;

import java.util.Optional;

import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Positive;

import org.hibernate.validator.constraints.Length;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.util.UriComponentsBuilder;

import com.org.lob.project.api.model.ErrorMessage;
import com.org.lob.project.repository.entity.Customer;
import com.org.lob.project.service.CustomerService;

@RestController
@RequestMapping(REQUEST_MAPPING_CUSTOMER)
public class CustomerApi {

	private CustomerService customerService;

	public CustomerApi(CustomerService customerService) {
		this.customerService = customerService;
	}

	@GetMapping(path = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
	public ResponseEntity<?> getCustomerDetail(
			@PathVariable(name = PATH_VARIABLE_ID) @Length(min = 1) @Positive Long customerId) {
		Optional<Customer> customer = getCustomerById(customerId);
		if (customer.isEmpty()) {
			return new ResponseEntity<Void>(HttpStatus.NOT_FOUND);
		}
		return ResponseEntity.ok(customer.get());
	}

	@GetMapping(path = "/", produces = MediaType.APPLICATION_JSON_VALUE)
	public ResponseEntity<?> getAllCustomers(
			@RequestParam(name = REQUEST_PARAM_PAGE_NUMBER, required = true) @NotBlank(message = "{page_number.not_empty}") @Positive Integer pageNumber,
			@RequestParam(name = REQUEST_PARAM_PAGE_NUMBER, required = true) @Positive Integer pageSize) {
		try {
			Page<Customer> page = getCustomersPage(pageNumber, pageSize);
			return ResponseEntity.ok(page.getContent());
		} catch (Exception ex) {
			return handleException(ex);
		}
	}

	private Page<Customer> getCustomersPage(Integer pageNumber, Integer pageSize) {
		PageRequest pageRequest = PageRequest.of(pageNumber, pageSize);
		Page<Customer> page = customerService.findAll(pageRequest);
		return page;
	}

	@PostMapping(path = "/", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE)
	public ResponseEntity<?> createCustomer(@Valid @RequestBody Customer customer, UriComponentsBuilder ucBuilder) {
		try {

			if (customer.getId() != null) {
				return new ResponseEntity<Void>(HttpStatus.CONFLICT);
			}

			Customer createdCustomer = customerService.create(customer);

			return ResponseEntity
					.created(ucBuilder.path(REQUEST_MAPPING_CUSTOMER).buildAndExpand(createdCustomer.getId()).toUri())
					.body(createdCustomer);
		} catch (Exception ex) {
			return handleException(ex);
		}
	}

	@PutMapping(path = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE)
	public ResponseEntity<?> updateCustomer(
			@PathVariable(name = PATH_VARIABLE_ID) @NotBlank(message = "{id.not_empty}") @Length(min = 1) @Positive Long customerId,
			@RequestBody Customer customer) {
		try {
			Optional<Customer> customerOptional = getCustomerById(customerId);
			if (customerOptional.isEmpty()) {
				return new ResponseEntity<Void>(HttpStatus.NOT_FOUND);
			}
			customer.setId(customerId);
			Customer updatedCustomer = customerService.update(customer);
			return ResponseEntity.ok(updatedCustomer);
		} catch (Exception ex) {
			return handleException(ex);
		}
	}

	@DeleteMapping(path = "/{id}")
	public ResponseEntity<Void> deleteCustomer(
			@PathVariable(name = PATH_VARIABLE_ID) @NotBlank(message = "{id.not_empty}") @Length(min = 1) @Positive Long customerId) {
		Optional<Customer> customer = getCustomerById(customerId);
		if (customer.isEmpty()) {
			return new ResponseEntity<Void>(HttpStatus.NOT_FOUND);
		}
		customerService.deleteCustomer(null);
		return new ResponseEntity<Void>(HttpStatus.OK);
	}

	private Optional<Customer> getCustomerById(Long customerId) {
		return customerService.getCustomerById(customerId);
	}

	private ResponseEntity<ErrorMessage> handleException(Exception ex) {
		ex.printStackTrace();
		ErrorMessage error = new ErrorMessage(HttpStatus.BAD_REQUEST.value(), ex.getMessage());
		return ResponseEntity.badRequest().body(error);
	}
}

Also See