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

Rabbit MQ Up And Running In Kubernetes Cluster

Deploy RBAC

rbac.yaml

---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: rabbitmq
---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: rabbitmq
rules:
- apiGroups: [""]
  resources: ["endpoints"]
  verbs: ["get"]
- apiGroups: [""]
  resources: ["events"]
  verbs: ["create"]
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: rabbitmq
subjects:
- kind: ServiceAccount
  name: rabbitmq
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: rabbitmq

Deploy Configmap

configmap.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: rabbitmq-config
data:
  RMQ_ADMIN_USER: admin
  enabled_plugins: |
    [rabbitmq_peer_discovery_k8s, rabbitmq_management, rabbitmq_prometheus].
  rabbitmq.conf: |
    ## Clustering
    #cluster_formation.peer_discovery_backend = k8s
    cluster_formation.peer_discovery_backend = rabbit_peer_discovery_k8s
    cluster_formation.k8s.host = kubernetes.default.svc.cluster.local
    cluster_formation.k8s.address_type = hostname
    cluster_formation.k8s.service_name = rabbitmq-headless
    cluster_partition_handling = autoheal

    #cluster_formation.k8s.hostname_suffix = rabbitmq.${NAMESPACE}.svc.cluster.local
    #cluster_formation.node_cleanup.interval = 10
    #cluster_formation.node_cleanup.only_log_warning = true
    
    ## queue master locator
    queue_master_locator=min-masters
    loopback_users.guest = false

    auth_mechanisms.1 = PLAIN
    auth_mechanisms.2 = AMQPLAIN

    ## set max memory available to MQ
    #vm_memory_high_watermark.absolute = 1GB
    vm_memory_high_watermark.absolute = 900MB
    ## load definitions file
    management.load_definitions = /etc/rabbitmq/definitions.json

    management.path_prefix = /mqadmin

Deploy  Secrets

definitions.json

{
    "users": [
      {
        "name": "proj_mq_dev",
        "password": "<PWD>",
        "tags": ""
      },
      {
        "name": "admin",
        "password": "<PWD>",
        "tags": "administrator"
      }
    ],
    "vhosts":[
        {"name":"/"}
    ],
    "policies":[
        {"vhost":"/","name":"ha","pattern":"", "definition":{"ha-mode":"all","ha-sync-mode":"automatic","ha-sync-batch-size":256}}
    ],
    "permissions": [
      {
        "user": "proj_mq_dev",
        "vhost": "/",
        "configure": ".*",
        "write": ".*",
        "read": ".*"
      },
      {
        "user": "admin",
        "vhost": "/",
        "configure": ".*",
        "write": ".*",
        "read": ".*"
      }
    ]      
    }

secrets.yaml

apiVersion: v1
kind: Secret
metadata:
  name: rabbitmq-secrets
type: Opaque
data:
  RMQ_ERLANG_COOKIE: Wm1GclpWOXdZWA==
  definitions.json: >-
    <Base 64 encoded definitions.json>

Deploy Services

deploy headless service and client-service-ci

headless-service.yaml

# Headless service that makes it possible to lookup individual rabbitmq nodes
apiVersion: v1
kind: Service
metadata:
  name: rabbitmq-headless
spec:
  clusterIP: None
  ports:
  - name: epmd
    port: 4369
    protocol: TCP
    targetPort: 4369
  - name: cluster-rpc
    port: 25672
    protocol: TCP
    targetPort: 25672
  selector:
    app: rabbitmq
  type: ClusterIP
  sessionAffinity: None

client-service-ci.yaml

kind: Service
apiVersion: v1
metadata:
  name: rabbitmq-client
  labels:
    app: rabbitmq
spec:
  type: ClusterIP
  ports:
   - name: http
     protocol: TCP
     port: 15672
     targetPort: management
   - name: prometheus
     protocol: TCP
     port: 15692
     targetPort: prometheus
   - name: amqp
     protocol: TCP
     port: 5672
     targetPort: amqp
  selector:
    app: rabbitmq

client-service-lb.yaml

kind: Service
apiVersion: v1
metadata:
  name: rabbitmq-client
  labels:
    app: rabbitmq
    type: LoadBalancer
spec:
  type: LoadBalancer
  sessionAffinity: None
  loadBalancerIP: <External IP Address>
  externalTrafficPolicy: Cluster
  ports:
   - name: http
     protocol: TCP
     port: 15672
     targetPort: management
   - name: prometheus
     protocol: TCP
     port: 15692
     targetPort: prometheus
   - name: amqp
     protocol: TCP
     port: 5672
     targetPort: amqp
  selector:
    app: rabbitmq

Deploy Statefulset

statefulset.yaml

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: rabbitmq
spec:
  selector:
    matchLabels:
      app: "rabbitmq"
  # headless service that gives network identity to the RMQ nodes, and enables them to cluster
  serviceName: rabbitmq-headless # serviceName is the name of the service that governs this StatefulSet. This service must exist before the StatefulSet, and is responsible for the network identity of the set. Pods get DNS/hostnames that follow the pattern: pod-specific-string.serviceName.default.svc.cluster.local where "pod-specific-string" is managed by the StatefulSet controller.
  replicas: 1
  volumeClaimTemplates:
  - metadata:
      name: rabbitmq-data
    spec:
      storageClassName: nas-thin
      accessModes:
      - ReadWriteOnce
      resources:
        requests:
          storage: "6Gi"
  template:
    metadata:
      name: rabbitmq
      labels:
        app: rabbitmq
    spec:
      initContainers:
      # Since k8s 1.9.4, config maps mount read-only volumes. Since the Docker image also writes to the config file,
      # the file must be mounted as read-write. We use init containers to copy from the config map read-only
      # path, to a read-write path
      - name: "rabbitmq-config"
        image: docker.repo1.uhc.com/busybox:1.32.0
        volumeMounts:
        - name: rabbitmq-config
          mountPath: /tmp/rabbitmq
        - name: rabbitmq-config-rw
          mountPath: /etc/rabbitmq
        - name: mq-secret-def
          mountPath: /tmp/rabbitsec
        command:
        - sh
        - -c
        # the newline is needed since the Docker image entrypoint scripts appends to the config file
        - cp /tmp/rabbitmq/rabbitmq.conf /etc/rabbitmq/rabbitmq.conf && echo '' >> /etc/rabbitmq/rabbitmq.conf;
          cp /tmp/rabbitmq/enabled_plugins /etc/rabbitmq/enabled_plugins;
          cp /tmp/rabbitsec/definitions.json /etc/rabbitmq/definitions.json
      volumes:
      - name: rabbitmq-config
        configMap:
          name: rabbitmq-config
          optional: false
          items:
          - key: enabled_plugins
            path: "enabled_plugins"
          - key: rabbitmq.conf
            path: "rabbitmq.conf"
      - name: mq-secret-def
        secret:
          secretName: rabbitmq-secrets
          items:
            - key: definitions.json
              path: definitions.json
      # read-write volume into which to copy the rabbitmq.conf and enabled_plugins files
      # this is needed since the docker image writes to the rabbitmq.conf file
      # and Kubernetes Config Maps are mounted as read-only since Kubernetes 1.9.4
      - name: rabbitmq-config-rw
        emptyDir: {}
      - name: rabbitmq-data
        persistentVolumeClaim:
          claimName: rabbitmq-data
      serviceAccount: rabbitmq
      # The Docker image runs as the `rabbitmq` user with uid 999 
      # and writes to the `rabbitmq.conf` file
      # The security context is needed since the image needs
      # permission to write to this file. Without the security 
      # context, `rabbitmq.conf` is owned by root and inaccessible
      # by the `rabbitmq` user
      securityContext:
        fsGroup: 999
        runAsUser: 999
        runAsGroup: 999
      containers:
      - name: rabbitmq
        # Community Docker Image
        image: docker.repo1.uhc.com/rabbitmq:3.8-management
        volumeMounts:
        # mounting rabbitmq.conf and enabled_plugins
        # this should have writeable access, this might be a problem
        - name: rabbitmq-config-rw
          mountPath: "/etc/rabbitmq"
          # mountPath: "/etc/rabbitmq/conf.d/"
        # rabbitmq data directory
        - name: rabbitmq-data
          mountPath: "/var/lib/rabbitmq/mnesia"
        env:
        - name: RABBITMQ_DEFAULT_USER
          value: "admin"
        - name: RABBITMQ_ERLANG_COOKIE
          valueFrom:
            secretKeyRef:
              name: rabbitmq-secrets
              key: RMQ_ERLANG_COOKIE
        ports:
        - name: amqp
          containerPort: 5672
          protocol: TCP
        - name: management
          containerPort: 15672
          protocol: TCP
        - name: prometheus
          containerPort: 15692
          protocol: TCP
        - name: epmd
          containerPort: 4369
          protocol: TCP
        resources:
            requests:
              memory: 1Gi
              cpu: '1'
            limits:
              memory: 1Gi
              cpu: '1'
        livenessProbe:
          exec:
            # This is just an example. There is no "one true health check" but rather
            # several rabbitmq-diagnostics commands that can be combined to form increasingly comprehensive
            # and intrusive health checks.
            # Learn more at https://www.rabbitmq.com/monitoring.html#health-checks.
            #
            # Stage 2 check:
            command: ["rabbitmq-diagnostics", "status"]
          initialDelaySeconds: 120
          # See https://www.rabbitmq.com/monitoring.html for monitoring frequency recommendations.
          periodSeconds: 60
          timeoutSeconds: 15
        readinessProbe: # probe to know when RMQ is ready to accept traffic
          exec:
            # This is just an example. There is no "one true health check" but rather
            # several rabbitmq-diagnostics commands that can be combined to form increasingly comprehensive
            # and intrusive health checks.
            # Learn more at https://www.rabbitmq.com/monitoring.html#health-checks.
            #
            # Stage 1 check:
            command: ["rabbitmq-diagnostics", "ping"]
          initialDelaySeconds: 20
          periodSeconds: 60
          timeoutSeconds: 10

mqadmin is accessible after proxying

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

Spring Batch XML To MySQL Using Spring Data Repository

Source Code Can Be Found Here

Add dependencies

		<dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-oxm</artifactId>
        </dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-batch</artifactId>
		</dependency>

BatchConfig.java

import java.net.MalformedURLException;

import javax.sql.DataSource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobExecutionListener;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.StepExecutionListener;
import org.springframework.batch.core.configuration.annotation.BatchConfigurer;
import org.springframework.batch.core.configuration.annotation.DefaultBatchConfigurer;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepScope;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.batch.item.adapter.ItemWriterAdapter;
import org.springframework.batch.item.xml.StaxEventItemReader;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.UrlResource;
import org.springframework.oxm.jaxb.Jaxb2Marshaller;

import com.org.lob.project.batch.CustomerProcessor;
import com.org.lob.project.batch.model.CustomerData;
import com.org.lob.project.repository.entity.Customer;
import com.org.lob.project.service.CustomerService;
import com.org.lob.support.batch.LoggingJobExecutionListener;
import com.org.lob.support.batch.LoggingStepExecutionListener;

@Configuration
@EnableBatchProcessing
public class BatchConfig {

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

	private JobBuilderFactory jobBuilderFactory;	
	private StepBuilderFactory stepBuilderFactory;	

	BatchConfig(JobBuilderFactory jobBuilderFactory, StepBuilderFactory stepBuilderFactory) {
		this.jobBuilderFactory = jobBuilderFactory;
		this.stepBuilderFactory = stepBuilderFactory;
	}

	@Bean
	JobExecutionListener loggingJobExecutionListener() {
		return new LoggingJobExecutionListener();
	}

	@Bean
	StepExecutionListener loggingStepExecutionListener() {
		return new LoggingStepExecutionListener();
	}

	@Bean
	BatchConfigurer batchConfigurer(DataSource dataSource) {
		return new DefaultBatchConfigurer(dataSource);
	}

	@Bean
	Job processJob(Step step1, @Value("${app.batch_process.job.name}") String jobName, JobExecutionListener executionListener) {
		return jobBuilderFactory.get(jobName)
				.incrementer(new RunIdIncrementer())
				.listener(executionListener)
				.flow(step1)
				.end()
				.build();
	}

	@Bean
	Step step1(@Value("${app.batch_process.step1.name}") String stepName, StaxEventItemReader<CustomerData> reader, CustomerProcessor processor, ItemWriterAdapter<Customer> writer, StepExecutionListener stepExecutionListener) {
		return stepBuilderFactory.get(stepName)
				.listener(stepExecutionListener)
				.<CustomerData, Customer>chunk(10)
					.reader(reader)
					.processor(processor)
					.faultTolerant()
					//.skipPolicy(skip())
					.writer(writer)
				.build();
	}

	@Bean
	@StepScope
	StaxEventItemReader<CustomerData> customerDataReader(@Value("#{jobParameters['fileName']}") String file) throws MalformedURLException {

		LOGGER.info("StaxEventItemReader:fileName: {}", file);

		StaxEventItemReader<CustomerData> reader = new StaxEventItemReader<>();
		reader.setResource(new UrlResource(file));
		reader.setFragmentRootElementNames(new String[] { "customer" });
		reader.setUnmarshaller(newCustomerDataMarshaller());
		return reader;
	}

	private Jaxb2Marshaller newCustomerDataMarshaller() {
		Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
		marshaller.setClassesToBeBound(CustomerData.class);
		return marshaller;
	}

	@Bean
	CustomerProcessor customerProcessor() {
		return new CustomerProcessor();
	}

	@Bean
	ItemWriterAdapter<Customer> customerItemWriter(CustomerService customerService) {
		ItemWriterAdapter<Customer> writer = new ItemWriterAdapter<>();
		writer.setTargetObject(customerService);
		writer.setTargetMethod("create");
		return writer;
	}
}

Note: Following Tables would be created

because of the following

	@Bean
	BatchConfigurer batchConfigurer(DataSource dataSource) {
		return new DefaultBatchConfigurer(dataSource);
	}

DDL Scripts can be found here

LoggingJobExecutionListener.java

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.listener.JobExecutionListenerSupport;

public class LoggingJobExecutionListener extends JobExecutionListenerSupport {

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

	@Override
	public void beforeJob(JobExecution jobExecution) {
		LOGGER.info("Executing Job {} ", jobExecution.getJobInstance().getJobName());
	}

	@Override
	public void afterJob(JobExecution jobExecution) {
		LOGGER.info("Finished Job {} ", jobExecution.getJobInstance().getJobName(), jobExecution.getStatus());
		super.afterJob(jobExecution);
	}
}

LoggingStepExecutionListener.java

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.ExitStatus;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.listener.StepExecutionListenerSupport;

public class LoggingStepExecutionListener extends StepExecutionListenerSupport {
	
	private static final Logger LOGGER = LoggerFactory.getLogger(LoggingStepExecutionListener.class);

	@Override
	public void beforeStep(StepExecution stepExecution) {
		LOGGER.info("Executing Step {}", stepExecution.getStepName());
	}
	
	@Override
	public ExitStatus afterStep(StepExecution stepExecution) {
		LOGGER.info("Finished Step {} with staus {}", stepExecution.getStepName(), stepExecution.getExitStatus());
		return stepExecution.getExitStatus();
	}

}

CustomerProcessor.java

import java.util.List;
import java.util.stream.Collectors;

import org.springframework.batch.item.ItemProcessor;

import com.org.lob.project.batch.model.AddressData;
import com.org.lob.project.batch.model.CustomerData;
import com.org.lob.project.repository.entity.Address;
import com.org.lob.project.repository.entity.Customer;

public class CustomerProcessor implements ItemProcessor<CustomerData, Customer>{

	@Override
	public Customer process(CustomerData item) throws Exception {
		Customer customer = new Customer();
		customer.setId(item.getId());
		customer.setEmailAddress(item.getEmailAddress());
		customer.setFirstName(item.getFirstName());
		customer.setLastName(item.getLastName());
		customer.setAddresses(buildAddress(item.getAddresses()));
		return customer;
	}

	private List<Address> buildAddress(List<AddressData> addresses) {
		return addresses.stream().map(ad -> buildAddrss(ad)).collect(Collectors.toList());
	}

	private Address buildAddrss(AddressData ad) {
		Address address = new Address();
		address.setCity(ad.getCity());
		address.setStateCode(ad.getStateCode());
		address.setCountry(ad.getCountry());
		address.setId(ad.getId());
		address.setStateCode(ad.getStateCode());
		address.setStreetAddress(ad.getStreetAddress());
		
		return address;
	}
}

CustomerData.java

import java.util.ArrayList;
import java.util.List;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement(name = "customer")
@XmlAccessorType(XmlAccessType.FIELD)
public class CustomerData {

	private Long id;
	private String firstName;
	private String lastName;
	private String emailAddress;

	@XmlElementWrapper(name = "addresses")
	@XmlElement(name = "address")
	private List<AddressData> 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<AddressData> getAddresses() {
		return addresses;
	}

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

}

AddressData.java

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;

@XmlRootElement(name="address")
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(propOrder = { "id", "zipCode", "stateCode", "country", "streetAddress", "city"})
public class AddressData {

    private Long id;

    @XmlElement(name = "street-address")
    private String streetAddress;

    private String city;

    @XmlElement(name = "state-code")
    private String stateCode;

    private  String country;

    @XmlElement(name = "zip-code")
    private String zipCode;
    
    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;
	}
}

BatchProcessMQConsumer.java

import java.util.Date;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.JobParametersInvalidException;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException;
import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException;
import org.springframework.batch.core.repository.JobRestartException;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.org.lob.project.messaging.model.BatchProcessEvent;

@Component
public class BatchProcessMQConsumer {

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

	private ObjectMapper mapper;
	private JobLauncher jobLauncher;
	private Job job;

	public BatchProcessMQConsumer(ObjectMapper mapper, JobLauncher jobLauncher, Job job) {
		this.mapper = mapper;
		this.jobLauncher = jobLauncher;
		this.job = job;
	}

	@RabbitListener(queues = "#{'${rabbitmq.batch_process.triggered.queue}'}")
	public void consumeMessage(String message) {
		try {
			LOGGER.trace("Message received: {} ", message);

			JobExecution execution = launchJob(create(message));

			LOGGER.debug("Message processed successfully with status {} ", execution.getExitStatus());
		} catch (Exception e) {
			LOGGER.error("Unnable to process the Message", e);
		}
	}

	private BatchProcessEvent create(String message) throws JsonProcessingException {
		return this.mapper.readValue(message, BatchProcessEvent.class);
	}

	private JobExecution launchJob(BatchProcessEvent batchProcessEvent) throws JobExecutionAlreadyRunningException, JobRestartException,
			JobInstanceAlreadyCompleteException, JobParametersInvalidException {
		JobParameters params = jobParams(batchProcessEvent);
		return this.jobLauncher.run(this.job, params);
	}

	private JobParameters jobParams(BatchProcessEvent batchProcessEvent) {
		return new JobParametersBuilder()
				.addString("JobID", String.valueOf(System.currentTimeMillis()))
				.addString("fileName", batchProcessEvent.getFilePath())
				.addDate("launchDate", new Date())
				.toJobParameters();
	}
}

Publishing Message From Rabbit Management Console

E:\eWorkspaces\job\customer.xml

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<customers>
	<customer>
		<firstName>firstname_batch</firstName>
		<lastName>lastName_batch</lastName>
		<emailAddress>[email protected]</emailAddress>
		<addresses>
			<address>
				<zip-code>ZC</zip-code>
				<state-code>SC</state-code>
				<country>copuntyhr</country>
				<street-address>Str Adr</street-address>
				<city>city</city>
			</address>
			<address>
				<zip-code>ZC1</zip-code>
				<state-code>SC1</state-code>
				<country>copuntyhr1</country>
				<street-address>Str Adr1</street-address>
				<city>city1</city>
			</address>
		</addresses>
	</customer>

</customers>

content_type = text/plain
content_encoding = UTF-8
{"id":1, "path": "E:\\eWorkspaces\\job\\customer.xml"}

Data Records would be created in DB

Note

Spring Batch distinguishes jobs based on the JobParameters. So if you always pass different JobParameters to the same job, you will have multiple instances of the same job running at the same time

Also See

References

Maintaining Data Revision History With Spring Data Envers

Refer this for basic Auditing

Source Code can be found here

Add dependency

        <dependency>
		    <groupId>org.springframework.data</groupId>
		    <artifactId>spring-data-envers</artifactId>
		</dependency>

Which would pull in the following dependencies

Enable Envers

Annotate your repository Config with @EnableEnversRepositories

Add repositoryFactoryBeanClass = EnversRevisionRepositoryFactoryBean.class to @EnableJpaRepositories

Better option would be use below

Mark An Entity as @Audited

@NotAudited

If you dont want the relationship to be audited mark it as org.hibernate.envers.NotAudited

Extend your Repository with RevisionRepository

We get the following methods after extending with RevisionRepository

Tables Created

Hibernate: 
    
    create table prj_address (
       id bigint not null auto_increment,
        created_by varchar(25),
        create_date_time timestamp default '2021-06-10 20:47:05.967394',
        modified_by varchar(25),
        modified_date_time timestamp default '2021-06-10 20:47:05.967394',
        city varchar(255),
        country varchar(255),
        state_code varchar(255),
        street_address varchar(255),
        zip_code varchar(255),
        customer_id bigint,
        primary key (id)
    ) engine=InnoDB
Hibernate: 
    
    create table prj_customer (
       id bigint not null auto_increment,
        created_by varchar(25),
        create_date_time timestamp default '2021-06-10 20:47:05.967394',
        modified_by varchar(25),
        modified_date_time timestamp default '2021-06-10 20:47:05.967394',
        email_address varchar(255),
        first_name varchar(255),
        last_name varchar(255),
        primary key (id)
    ) engine=InnoDB
Hibernate: 
    
    create table prj_customer_aud (
       id bigint not null,
        rev integer not null,
        revtype tinyint,
        email_address varchar(255),
        first_name varchar(255),
        last_name varchar(255),
        primary key (id, rev)
    ) engine=InnoDB
Hibernate: 
    
    create table revinfo (
       rev integer not null auto_increment,
        revtstmp bigint,
        primary key (rev)
    ) engine=InnoDB
Hibernate: 
    
    alter table prj_address 
       add constraint FKk5h0gt5brecxp11or1okmsslr 
       foreign key (customer_id) 
       references prj_customer (id)
Hibernate: 
    
    alter table prj_customer_aud 
       add constraint FK119fx2iv1tqbhvjopxr2f6age 
       foreign key (rev) 
       references revinfo (rev)

Testing

Create

curl --location --request POST 'http://localhost:8091/api/v1/customer/' \
--header 'Content-Type: application/json' \
--data-raw '{
    "firstName": "Nadeem",
    "lastName": "Mohammad",
    "emailAddress" : "[email protected]",
    "addresses": [
        {
            "streetAddress": "afd",
            "city": "afds",
            "stateCode": "asdf",
            "country": "sfdds",
            "zipCode": "432423"
        },
        {
            "streetAddress": "asdf",
            "city": "ffff",
            "stateCode": "sdf",
            "country": "gfdg",
            "zipCode": "444"
        }
    ]
}'

Update

curl --location --request PUT 'http://localhost:8091/api/v1/customer/1' \
--header 'Content-Type: application/json' \
--data-raw '{

    "firstName": "Nadeem1",
    "lastName": "Mohammad",
    "emailAddress" : "[email protected]",
    "addresses": [
        {
            "streetAddress": "afd",
            "city": "afds",
            "stateCode": "asdf",
            "country": "sfdds",
            "zipCode": "432423"
        },
        {
            "streetAddress": "asdf",
            "city": "ffff",
            "stateCode": "sdf",
            "country": "gfdg",
            "zipCode": "444"
        }
    ]
}'

Revision added

RevisionTypes

Actual entity updated

Deletion

curl --location --request DELETE 'http://localhost:8091/api/v1/customer/1' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJtbmFkZWVtIiwibmFtZSI6Ik1vaGFtbWFkIE5hZGVlbSIsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNjczMDQyNDU3NjI5fQ.gT8xVbDGJJQK8tsID6CCdBZCftnUVn7Tud-v1NAOHhw'

record #1 deleted

Audit table updated with delete revision type

corresponding revision is deleted from REVINFO table as well

SQL which are executed

Hibernate: delete from PRJ_ADDRESS where ID=?
Hibernate: delete from PRJ_ADDRESS where ID=?
Hibernate: delete from PRJ_CUSTOMER where ID=?
Hibernate: insert into REVINFO (REVTSTMP) values (?)
Hibernate: insert into PRJ_CUSTOMER_AUD (REVISION_TYPE, CREATED_BY, CREATE_DATE_TIME, MODIFIED_BY, MODIFIED_DATE_TIME, EMAIL_ADDRESS, FIRST_NAME, LAST_NAME, ID, REVISION_ID) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)

Note

Extending com.org.lob.project.repository.entity.Auditable does not create audit fields in _audit table, a solution to this is to not to extend entity classes, simply add the columns to child entity.

Another solution would be use org.hibernate.envers.AuditOverride

Here are the properties you can customize

spring.jpa.properties.org.hibernate.envers.audit_table_suffix=_AUD
spring.jpa.properties.org.hibernate.envers.revision_field_name=REVISION_ID
spring.jpa.properties.org.hibernate.envers.revision_type_field_name=REVISION_TYPE

Also See

References

Spring Boot Bean Validation

1 Basic Validation

Step 1 : Add Dependency

<dependency> 
    <groupId>org.springframework.boot</groupId> 
    <artifactId>spring-boot-starter-validation</artifactId> 
</dependency>

Step 2 : Annotate fields

Step 3 : Validate

Step 4 : Exception Handler

2 Reading Error Message From Properties File

Step 1 : Add LocalValidatorFactoryBean

import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;

@Configuration
public class BeanValidatorConfig {

	@Bean
	MessageSource messageSource() {
		ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();

		messageSource.setBasename("classpath:messages");
		messageSource.setDefaultEncoding("UTF-8");
		return messageSource;
	}

	@Bean
	LocalValidatorFactoryBean getValidator(MessageSource messageSource) {
	    LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
	    bean.setValidationMessageSource(messageSource);
	    return bean;
	}
}

Step 2 : Add message key

Should be within curly braces

@NotBlank(message = "{email.notempty}")

Step 3 : Add properties files

Step 4 : Validate

Constraint List

You must be wondering what is the use of List, found in most of the Constraints

It is basically used for groups, used in conjunction with org.springframework.validation.annotation.Validated

@NotNull.List({@NotNull(groups=Create.class,message="Some message!"), 
               @NotNull(groups=Update.class, message="Some other message!"})
private String email;
public User register(@Validated(Create.class) User u) {
    return service.createAppUser(u);
}

3 Implementing Custom Validator

Step 1 : Create Custom Constraint Annotation

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Documented;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;
import javax.validation.ReportAsSingleViolation;

import com.org.lob.support.URL.List;

@Documented
@Constraint(validatedBy = URLValidator.class)
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
@ReportAsSingleViolation
public @interface URL {
	String message() default "{org.hibernate.validator.constraints.URL.message}";

	Class<?>[] groups() default { };

	Class<? extends Payload>[] payload() default { };

	/**
	 * @return the protocol (scheme) the annotated string must match, e.g. ftp or http.
	 *         Per default any protocol is allowed
	 */
	String protocol() default "";

	/**
	 * @return the host the annotated string must match, e.g. localhost. Per default any host is allowed
	 */
	String host() default "";

	/**
	 * @return the port the annotated string must match, e.g. 80. Per default any port is allowed
	 */
	int port() default -1;

	/**
	 * Defines several {@code @URL} annotations on the same element.
	 */
	@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
	@Retention(RUNTIME)
	@Documented
	public @interface List {
		URL[] value();
	}
}

Step 2 : Implement ConstraintValidator

import java.net.MalformedURLException;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class URLValidator implements ConstraintValidator<org.hibernate.validator.constraints.URL, CharSequence> {
	private String protocol;
	private String host;
	private int port;

	@Override
	public void initialize(org.hibernate.validator.constraints.URL url) {
		this.protocol = url.protocol();
		this.host = url.host();
		this.port = url.port();
	}

	@Override
	public boolean isValid(CharSequence value, ConstraintValidatorContext constraintValidatorContext) {
		if ( value == null || value.length() == 0 ) {
			return true;
		}

		java.net.URL url;
		try {
			url = new java.net.URL( value.toString() );
		}
		catch (MalformedURLException e) {
			return false;
		}

		if ( protocol != null && protocol.length() > 0 && !url.getProtocol().equals( protocol ) ) {
			return false;
		}

		if ( host != null && host.length() > 0 && !url.getHost().equals( host ) ) {
			return false;
		}

		if ( port != -1 && url.getPort() != port ) {
			return false;
		}

		return true;
	}
}

Step 3 : Use it

  @URL 
  private String url;

Another Example

Annotation

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;

@Documented
@Constraint(validatedBy = CommunicationTypeValidator.class)
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface CommunicationType {

	String message() default "{org.project.validator.constraints.CommunicationType.message}";

	Class<?>[] groups() default {};

	Class<? extends Payload>[] payload() default {};
}

Validator

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

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class CommunicationTypeValidator implements ConstraintValidator<CommunicationType, String> {

	private final List<String> commPreferences = Arrays.asList("email", "mobilePhone");

	@Override
	public boolean isValid(String value, ConstraintValidatorContext context) {
		return commPreferences.contains(value);
	}
}

Usage

    @NotEmpty(message = "Communication type is required")
    @CommunicationType
    private String communicationType;

4 Custom Class Level Validation

Step 1 : Create Annotation

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import javax.validation.Constraint;

@Constraint(validatedBy = FieldsValueMatchValidator.class)
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface FieldsValueMatch {

    String message() default "Fields values don't match!";

    String field();

    String fieldMatch();

    @Target({ ElementType.TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    @interface List {
        FieldsValueMatch[] value();
    }
}

Step 2 : Create Validator

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

import org.springframework.beans.BeanWrapperImpl;

public class FieldsValueMatchValidator implements ConstraintValidator<FieldsValueMatch, Object> {

	private String field;
	private String fieldMatch;

	public void initialize(FieldsValueMatch constraintAnnotation) {
		this.field = constraintAnnotation.field();
		this.fieldMatch = constraintAnnotation.fieldMatch();
	}

	public boolean isValid(Object value, ConstraintValidatorContext context) {

		Object fieldValue = new BeanWrapperImpl(value).getPropertyValue(field);
		Object fieldMatchValue = new BeanWrapperImpl(value).getPropertyValue(fieldMatch);

		if (fieldValue != null) {
			return fieldValue.equals(fieldMatchValue);
		} else {
			return fieldMatchValue == null;
		}
	}
}

Step 3 : Use the Annotation

@FieldsValueMatch.List({ 
    @FieldsValueMatch(
      field = "password", 
      fieldMatch = "verifyPassword", 
      message = "Passwords do not match!"
    ), 
    @FieldsValueMatch(
      field = "email", 
      fieldMatch = "verifyEmail", 
      message = "Email addresses do not match!"
    )
})
public class User {
	private String email;
	private String verifyEmail;
	private String password;
	private String verifyPassword;
}

Also See

References