Source code can be found here

Source code can be found here

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 + "]";
}
}
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;
}
}
@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;
}
}
Source Code can be found here
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>

We have created the Models AddressModel and CustomerModel
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);
}
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


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));
}
}
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
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

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 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

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

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());
}
}
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);
}
}
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

Source Code of this project can be found here
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 + "]";
}
}
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);
}
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");
}
}
}
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);
}
}
Source Code Can Be Found Here

<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();
}
}
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

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
Refer this for basic Auditing
Source Code can be found here
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-envers</artifactId>
</dependency>
Which would pull in the following dependencies

Annotate your repository Config with @EnableEnversRepositories
Add repositoryFactoryBeanClass = EnversRevisionRepositoryFactoryBean.class to @EnableJpaRepositories

Better option would be use below


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


We get the following methods after extending with RevisionRepository

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)


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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
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
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>






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;
}
}
Should be within curly braces
@NotBlank(message = "{email.notempty}")



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);
}
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();
}
}
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;
}
}
@URL
private String url;
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;
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();
}
}
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;
}
}
}
@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;
}