Customizing HATEOAS link generation for entities with composite ids - spring

I have configured a RepositoryRestResource on a PageAndSortingRepository that accesses an Entity that includes a composite Id:
#Entity
#IdClass(CustomerId.class)
public class Customer {
#Id BigInteger id;
#Id int startVersion;
...
}
public class CustomerId {
BigInteger id;
int startVersion;
...
}
#RepositoryRestResource(collectionResourceRel = "customers", path = "customers", itemResourceRel = "customers/{id}_{startVersion}")
public interface CustomerRepository extends PagingAndSortingRepository<Customer, CustomerId> {}
When i access the server at "http://<server>/api/customers/1_1" for instance, I get the correct resource back as json, but the href in the _links section for self is the wrong and also the same for any other customer i query: "http://<server>/api/customer/1"
i.e.:
{
"id" : 1,
"startVersion" : 1,
...
"firstname" : "BOB",
"_links" : {
"self" : {
"href" : "http://localhost:9081/reps/api/reps/1" <-- This should be /1_1
}
}
}
I suppose this is because of my composite Id, But I am chuffed as to how i can change this default behaviour.
I've had a look at the ResourceSupport and the ResourceProcessor class but am not sure how much i need to change in order fix this issue.
Can someone who knows spring lend me a hand?

Unfortunately, all Spring Data JPA/Rest versions up to 2.1.0.RELEASE are not able to serve your need out of the box.
The source is buried inside Spring Data Commons/JPA itself. Spring Data JPA supports only Id and EmbeddedId as identifier.
Excerpt JpaPersistentPropertyImpl:
static {
// [...]
annotations = new HashSet<Class<? extends Annotation>>();
annotations.add(Id.class);
annotations.add(EmbeddedId.class);
ID_ANNOTATIONS = annotations;
}
Spring Data Commons doesn't support the notion of combined properties. It treats every property of a class independently from each other.
Of course, you can hack Spring Data Rest. But this is cumbersome, doesn't solve the problem at its heart and reduces the flexibility of the framework.
Here's the hack. This should give you an idea how to tackle your problem.
In your configuration override repositoryExporterHandlerAdapter and return a CustomPersistentEntityResourceAssemblerArgumentResolver.
Additionally, override backendIdConverterRegistry and add CustomBackendIdConverter to the list of known id converter:
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.rest.core.projection.ProxyProjectionFactory;
import org.springframework.data.rest.webmvc.RepositoryRestHandlerAdapter;
import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration;
import org.springframework.data.rest.webmvc.spi.BackendIdConverter;
import org.springframework.data.rest.webmvc.support.HttpMethodHandlerMethodArgumentResolver;
import org.springframework.data.web.config.EnableSpringDataWebSupport;
import org.springframework.hateoas.ResourceProcessor;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.plugin.core.OrderAwarePluginRegistry;
import org.springframework.plugin.core.PluginRegistry;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
#Configuration
#Import(RepositoryRestMvcConfiguration.class)
#EnableSpringDataWebSupport
public class RestConfig extends RepositoryRestMvcConfiguration {
#Autowired(required = false) List<ResourceProcessor<?>> resourceProcessors = Collections.emptyList();
#Autowired
ListableBeanFactory beanFactory;
#Override
#Bean
public PluginRegistry<BackendIdConverter, Class<?>> backendIdConverterRegistry() {
List<BackendIdConverter> converters = new ArrayList<BackendIdConverter>(3);
converters.add(new CustomBackendIdConverter());
converters.add(BackendIdConverter.DefaultIdConverter.INSTANCE);
return OrderAwarePluginRegistry.create(converters);
}
#Bean
public RequestMappingHandlerAdapter repositoryExporterHandlerAdapter() {
List<HttpMessageConverter<?>> messageConverters = defaultMessageConverters();
configureHttpMessageConverters(messageConverters);
RepositoryRestHandlerAdapter handlerAdapter = new RepositoryRestHandlerAdapter(defaultMethodArgumentResolvers(),
resourceProcessors);
handlerAdapter.setMessageConverters(messageConverters);
return handlerAdapter;
}
private List<HandlerMethodArgumentResolver> defaultMethodArgumentResolvers()
{
CustomPersistentEntityResourceAssemblerArgumentResolver peraResolver = new CustomPersistentEntityResourceAssemblerArgumentResolver(
repositories(), entityLinks(), config().projectionConfiguration(), new ProxyProjectionFactory(beanFactory));
return Arrays.asList(pageableResolver(), sortResolver(), serverHttpRequestMethodArgumentResolver(),
repoRequestArgumentResolver(), persistentEntityArgumentResolver(),
resourceMetadataHandlerMethodArgumentResolver(), HttpMethodHandlerMethodArgumentResolver.INSTANCE,
peraResolver, backendIdHandlerMethodArgumentResolver());
}
}
Create CustomBackendIdConverter. This class is responsible for rendering your custom entity ids:
import org.springframework.data.rest.webmvc.spi.BackendIdConverter;
import java.io.Serializable;
public class CustomBackendIdConverter implements BackendIdConverter {
#Override
public Serializable fromRequestId(String id, Class<?> entityType) {
return id;
}
#Override
public String toRequestId(Serializable id, Class<?> entityType) {
if(entityType.equals(Customer.class)) {
Customer c = (Customer) id;
return c.getId() + "_" +c.getStartVersion();
}
return id.toString();
}
#Override
public boolean supports(Class<?> delimiter) {
return true;
}
}
CustomPersistentEntityResourceAssemblerArgumentResolver in turn should return a CustomPersistentEntityResourceAssembler:
import org.springframework.core.MethodParameter;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.core.projection.ProjectionDefinitions;
import org.springframework.data.rest.core.projection.ProjectionFactory;
import org.springframework.data.rest.webmvc.PersistentEntityResourceAssembler;
import org.springframework.data.rest.webmvc.config.PersistentEntityResourceAssemblerArgumentResolver;
import org.springframework.data.rest.webmvc.support.PersistentEntityProjector;
import org.springframework.hateoas.EntityLinks;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.ModelAndViewContainer;
public class CustomPersistentEntityResourceAssemblerArgumentResolver extends PersistentEntityResourceAssemblerArgumentResolver {
private final Repositories repositories;
private final EntityLinks entityLinks;
private final ProjectionDefinitions projectionDefinitions;
private final ProjectionFactory projectionFactory;
public CustomPersistentEntityResourceAssemblerArgumentResolver(Repositories repositories, EntityLinks entityLinks,
ProjectionDefinitions projectionDefinitions, ProjectionFactory projectionFactory) {
super(repositories, entityLinks,projectionDefinitions,projectionFactory);
this.repositories = repositories;
this.entityLinks = entityLinks;
this.projectionDefinitions = projectionDefinitions;
this.projectionFactory = projectionFactory;
}
public boolean supportsParameter(MethodParameter parameter) {
return PersistentEntityResourceAssembler.class.isAssignableFrom(parameter.getParameterType());
}
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
String projectionParameter = webRequest.getParameter(projectionDefinitions.getParameterName());
PersistentEntityProjector projector = new PersistentEntityProjector(projectionDefinitions, projectionFactory,
projectionParameter);
return new CustomPersistentEntityResourceAssembler(repositories, entityLinks, projector);
}
}
CustomPersistentEntityResourceAssembler needs to override getSelfLinkFor. As you can see entity.getIdProperty() return either id or startVersion property of your Customer class which in turn gets used to retrieve the real value with the help of a BeanWrapper. Here we are short circuit the whole framework with the use of instanceof operator. Hence your Customer class should implement Serializable for further processing.
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.model.BeanWrapper;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.webmvc.PersistentEntityResourceAssembler;
import org.springframework.data.rest.webmvc.support.Projector;
import org.springframework.hateoas.EntityLinks;
import org.springframework.hateoas.Link;
import org.springframework.util.Assert;
public class CustomPersistentEntityResourceAssembler extends PersistentEntityResourceAssembler {
private final Repositories repositories;
private final EntityLinks entityLinks;
public CustomPersistentEntityResourceAssembler(Repositories repositories, EntityLinks entityLinks, Projector projector) {
super(repositories, entityLinks, projector);
this.repositories = repositories;
this.entityLinks = entityLinks;
}
public Link getSelfLinkFor(Object instance) {
Assert.notNull(instance, "Domain object must not be null!");
Class<? extends Object> instanceType = instance.getClass();
PersistentEntity<?, ?> entity = repositories.getPersistentEntity(instanceType);
if (entity == null) {
throw new IllegalArgumentException(String.format("Cannot create self link for %s! No persistent entity found!",
instanceType));
}
Object id;
//this is a hack for demonstration purpose. don't do this at home!
if(instance instanceof Customer) {
id = instance;
} else {
BeanWrapper<Object> wrapper = BeanWrapper.create(instance, null);
id = wrapper.getProperty(entity.getIdProperty());
}
Link resourceLink = entityLinks.linkToSingleResource(entity.getType(), id);
return new Link(resourceLink.getHref(), Link.REL_SELF);
}
}
That's it! You should see this URIs:
{
"_embedded" : {
"customers" : [ {
"name" : "test",
"_links" : {
"self" : {
"href" : "http://localhost:8080/demo/customers/1_1"
}
}
} ]
}
}
Imho, if you are working on a green field project I would suggest to ditch IdClass entirely and go with technical simple ids based on Long class. This was tested with Spring Data Rest 2.1.0.RELEASE, Spring data JPA 1.6.0.RELEASE and Spring Framework 4.0.3.RELEASE.

Although not desirable, I have worked around this issue by using an #EmbeddedId instead of a IdClass annotation on my JPA entity.
Like so:
#Entity
public class Customer {
#EmbeddedId
private CustomerId id;
...
}
public class CustomerId {
#Column(...)
BigInteger key;
#Column(...)
int startVersion;
...
}
I now see the correctly generated links 1_1 on my returned entities.
If anyone can still direct me to a solution that does not require I change the representation of my model, It would be highly appreciated. Luckily I had not progressed far in my application development for this to be of serious concern in changing, but I imagine that for others, there would be significant overhead in performing a change like this: (e.g. changing all queries that reference this model in JPQL queries).

I had a similar problem where the composite key scenarios for data rest was not working. #ksokol detailed explanation provided the necessary inputs to solve the issue. changed my pom primarily for data-rest-webmvc and data-jpa as
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-rest-webmvc</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>1.7.1.RELEASE</version>
</dependency>
which solved all the issues related to composite key and I need not do the customization. Thanks ksokol for the detailed explanation.

First, create a SpringUtil to get bean from spring.
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
#Component
public class SpringUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext;
#Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if(SpringUtil.applicationContext == null) {
SpringUtil.applicationContext = applicationContext;
}
}
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
public static Object getBean(String name){
return getApplicationContext().getBean(name);
}
public static <T> T getBean(Class<T> clazz){
return getApplicationContext().getBean(clazz);
}
public static <T> T getBean(String name,Class<T> clazz){
return getApplicationContext().getBean(name, clazz);
}
}
Then, implement BackendIdConverter.
import com.alibaba.fastjson.JSON;
import com.example.SpringUtil;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.rest.webmvc.spi.BackendIdConverter;
import org.springframework.stereotype.Component;
import javax.persistence.EmbeddedId;
import javax.persistence.Id;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.net.URLDecoder;
import java.net.URLEncoder;
#Component
public class CustomBackendIdConverter implements BackendIdConverter {
#Override
public boolean supports(Class<?> delimiter) {
return true;
}
#Override
public Serializable fromRequestId(String id, Class<?> entityType) {
if (id == null) {
return null;
}
//first decode url string
if (!id.contains(" ") && id.toUpperCase().contains("%7B")) {
try {
id = URLDecoder.decode(id, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
//deserialize json string to ID object
Object idObject = null;
for (Method method : entityType.getDeclaredMethods()) {
if (method.isAnnotationPresent(Id.class) || method.isAnnotationPresent(EmbeddedId.class)) {
idObject = JSON.parseObject(id, method.getGenericReturnType());
break;
}
}
//get dao class from spring
Object daoClass = null;
try {
daoClass = SpringUtil.getBean(Class.forName("com.example.db.dao." + entityType.getSimpleName() + "DAO"));
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
//get the entity with given primary key
JpaRepository simpleJpaRepository = (JpaRepository) daoClass;
Object entity = simpleJpaRepository.findOne((Serializable) idObject);
return (Serializable) entity;
}
#Override
public String toRequestId(Serializable id, Class<?> entityType) {
if (id == null) {
return null;
}
String jsonString = JSON.toJSONString(id);
String encodedString = "";
try {
encodedString = URLEncoder.encode(jsonString, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return encodedString;
}
}
After that. you can do what you want.
There is a sample below.
If the entity has single property pk, you can use
localhost:8080/demo/1 as normal. According to my code, suppose the pk
has annotation "#Id".
If the entity has composed pk, suppose the pk is demoId type, and has
annotation "#EmbeddedId", you can use localhost:8080/demo/{demoId
json} to get/put/delete. And your self link will be the same.

The answers provides above are helpful, but if you need a more generic approach that would be following -
package com.pratham.persistence.config;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sun.istack.NotNull;
import lombok.RequiredArgsConstructor;
import org.springframework.data.rest.webmvc.spi.BackendIdConverter;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import javax.persistence.EmbeddedId;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Base64;
import java.util.Optional;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
* Customization of how composite ids are exposed in URIs.
* The implementation will convert the Ids marked with {#link EmbeddedId} to base64 encoded json
* in order to expose them properly within URI.
*
* #author im-pratham
*/
#Component
#RequiredArgsConstructor
public class EmbeddedBackendIdConverter implements BackendIdConverter {
private final ObjectMapper objectMapper;
#Override
public Serializable fromRequestId(String id, Class<?> entityType) {
return getFieldWithEmbeddedAnnotation(entityType)
.map(Field::getType)
.map(ret -> {
try {
String decodedId = new String(Base64.getUrlDecoder().decode(id));
return (Serializable) objectMapper.readValue(decodedId, (Class) ret);
} catch (JsonProcessingException ignored) {
return null;
}
})
.orElse(id);
}
#Override
public String toRequestId(Serializable id, Class<?> entityType) {
try {
String json = objectMapper.writeValueAsString(id);
return Base64.getUrlEncoder().encodeToString(json.getBytes(UTF_8));
} catch (JsonProcessingException ignored) {
return id.toString();
}
}
#Override
public boolean supports(#NonNull Class<?> entity) {
return isEmbeddedIdAnnotationPresent(entity);
}
private boolean isEmbeddedIdAnnotationPresent(Class<?> entity) {
return getFieldWithEmbeddedAnnotation(entity)
.isPresent();
}
#NotNull
private static Optional<Field> getFieldWithEmbeddedAnnotation(Class<?> entity) {
return Arrays.stream(entity.getDeclaredFields())
.filter(method -> method.isAnnotationPresent(EmbeddedId.class))
.findFirst();
}
}

Related

Map/access array inside object on Pojo

How can I access and map an array inside an object on Pojo Spring Boot?
This is what I wanted to map
{
"customers": [
{
"customerId": "0434556574",
"paymentCode": "90501"
}
]
}
You would need to classes to match the JSON structure:
public class CustomersData {
private List<Customer> customers;
// Constructor, getter and setter
}
public class Customer {
private String customerId;
private String paymentCode;
// Constructor, getter and setter
}
Important thing is to make sure your JSON properties name matches the attributes name in your Java classes (thus avoiding explicit mapping between them).
Hi There are lot's of utility libraries you can convert JSON to required java class so that you can instantiate in right way.
Example based on your input:
-----------------------------------com.example.Customer.java-----------------------------------
package com.example;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.Generated;
import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
#JsonInclude(JsonInclude.Include.NON_NULL)
#JsonPropertyOrder({
"customerId",
"paymentCode"
})
#Generated("jsonschema2pojo")
public class Customer {
#JsonProperty("customerId")
private String customerId;
#JsonProperty("paymentCode")
private String paymentCode;
#JsonIgnore
private Map<String, Object> additionalProperties = new HashMap<String, Object>();
#JsonProperty("customerId")
public String getCustomerId() {
return customerId;
}
#JsonProperty("customerId")
public void setCustomerId(String customerId) {
this.customerId = customerId;
}
#JsonProperty("paymentCode")
public String getPaymentCode() {
return paymentCode;
}
#JsonProperty("paymentCode")
public void setPaymentCode(String paymentCode) {
this.paymentCode = paymentCode;
}
#JsonAnyGetter
public Map<String, Object> getAdditionalProperties() {
return this.additionalProperties;
}
#JsonAnySetter
public void setAdditionalProperty(String name, Object value) {
this.additionalProperties.put(name, value);
}
}
-----------------------------------com.example.Example.java-----------------------------------
package com.example;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Generated;
import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
#JsonInclude(JsonInclude.Include.NON_NULL)
#JsonPropertyOrder({
"customers"
})
#Generated("jsonschema2pojo")
public class Example {
#JsonProperty("customers")
private List<Customer> customers = null;
#JsonIgnore
private Map<String, Object> additionalProperties = new HashMap<String, Object>();
#JsonProperty("customers")
public List<Customer> getCustomers() {
return customers;
}
#JsonProperty("customers")
public void setCustomers(List<Customer> customers) {
this.customers = customers;
}
#JsonAnyGetter
public Map<String, Object> getAdditionalProperties() {
return this.additionalProperties;
}
#JsonAnySetter
public void setAdditionalProperty(String name, Object value) {
this.additionalProperties.put(name, value);
}
}
Reference :jsontopojo

Spring Boot MongoDB get only certain fields from database into model

I am trying to understand how to work with MongoDB and Spring Boot. I have a mongodb with sample data and I want it to return only selected fields and not the entire data.
My mongoDB collection contains data in the format
DAO.java
package com.books.Reading.DAO;
import com.books.Reading.model.booksModel;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.mongodb.repository.Query;
public interface readingListDAO extends MongoRepository<booksModel, String> {
#Query("{'title' : 'Griffon in Action'}")
public booksModel findFirstByOrderByTitleAsc();
}
model.java
package com.books.Reading.model;
import org.springframework.data.mongodb.core.mapping.Document;
#Document
public class booksModel {
String title;
String isbn;
int pageCount;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getIsbn() {
return isbn;
}
public void setIsbn(String isbn) {
this.isbn = isbn;
}
public int getPageCount() {
return pageCount;
}
public void setPageCount(int pageCount) {
this.pageCount = pageCount;
}
}
controller.java
package com.books.Reading.controller;
import com.books.Reading.model.booksModel;
import com.books.Reading.service.readingListService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
#RestController
public class readingListController {
#Autowired
private readingListService rlService;
#RequestMapping("/")
#ResponseBody
public booksModel getOne(){
return rlService.getOne();
}
}
service.java
package com.books.Reading.service;
import com.books.Reading.model.booksModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
#Service
public class readingListService {
#Autowired
private com.books.Reading.DAO.readingListDAO readingListDAO;
public booksModel getOne(){
System.out.println(readingListDAO.findFirstByOrderByTitleAsc());
return readingListDAO.findFirstByOrderByTitleAsc();
}
}
application.properties
spring.data.mongodb.host=127.0.0.1
spring.data.mongodb.port=27017
spring.data.mongodb.database=book
spring.session.mongodb.collection-name=readinglist
The MongoDB is hosted locally. How can I retrieve only selected fields (title, isbn and pageCount) from the database.
I am very new to Spring Boot, so if there are other errors or best practices, I welcome your feedback.
By default, all the properties of document will be fetched. if you want to ignore some of the properties in the result, you can use filter option.
#Query(value = "{'title' : ?0}", fields = "{'description' : 0}")
BooksModel findBooksModelByTitle(String title);

Entity not getting saved in EclipseLink EntityListener

I have written EntityListener using eclipseLink's "DescriptorEventAdapter". I tried almost all variations whatever present online BUT the entity which I am saving from my listener is not getting saved. I suspect something fishy is going on with transaction but didn't get the root cause. Here is the code :
package com.db;
import java.util.Date;
import javax.annotation.PostConstruct;
import javax.persistence.EntityManagerFactory;
import javax.transaction.Transactional;
import javax.transaction.Transactional.TxType;
import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.descriptors.DescriptorEvent;
import org.eclipse.persistence.descriptors.DescriptorEventAdapter;
import org.eclipse.persistence.jpa.JpaEntityManager;
import org.eclipse.persistence.queries.InsertObjectQuery;
import org.eclipse.persistence.queries.UpdateObjectQuery;
import org.eclipse.persistence.sessions.changesets.DirectToFieldChangeRecord;
import org.eclipse.persistence.sessions.changesets.ObjectChangeSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
#Component
public class NotificationMessageListener extends DescriptorEventAdapter {
public static Logger logger = LoggerFactory.getLogger(NotificationMessageListener.class);
private static final String targetColumn = "STATUS";
//private static AuditRepository auditRepo;
#Autowired
private StatusAuditRepository statusAuditRepo;
#Autowired
private RuleResultAuditRepository ruleResultRepo;
#Autowired
private EntityManagerFactory factory;
JpaEntityManager entityManager = null;
#PostConstruct
public void init() {
try {
entityManager = (JpaEntityManager) factory.createEntityManager();
// Use the entity manager to get a ClassDescriptor for the Entity class
ClassDescriptor desc =
entityManager.getSession().getClassDescriptor(NotificationMessage.class);
// Add this class as a listener to the class descriptor
desc.getEventManager().addListener(this);
} finally {
if (entityManager != null) {
// Cleanup the entity manager
entityManager.close();
}
}
}
/*#Autowired
public void setAuditRepo(AuditRepository auditRepo) {
NotificationMessageListener.auditRepo = auditRepo;
}*/
#Transactional(value = TxType.REQUIRES_NEW)
#Override
public void postInsert(DescriptorEvent event) {
logger.info("post insert is called ");
//NotificationMessage notificationMsg = (NotificationMessage) ((InsertObjectQuery) event.getQuery()).getObject();
//entityManager.getTransaction().begin();
NotificationStatusAudit statusAudit = new NotificationStatusAudit();
statusAudit.setInsertionTime(new Date());
//statusAudit.setNewVal(notificationMsg.getStatus());
statusAudit.setNewVal("abc");
statusAudit.setOldval("asdf");
statusAudit.setTargetColumnName("from listner");
//statusAudit.setTargetRecordId(notificationMsg.getId());
statusAudit.setTargetRecordId(123L);
statusAudit = statusAuditRepo.save(statusAudit);
//entityManager.getTransaction().commit();
//logger.info("Number of records "+statusAuditRepo.count());
//auditRuleResult(notificationMsg.getMessageCorrelationId() , true);
}
#Override
public void postUpdate(DescriptorEvent event) {
ObjectChangeSet objectChanges = ((UpdateObjectQuery) event.getQuery()).getObjectChangeSet();
DirectToFieldChangeRecord statusChanges = (DirectToFieldChangeRecord) objectChanges
.getChangesForAttributeNamed("status");
if (statusChanges != null && !statusChanges.getNewValue().equals(statusChanges.getOldValue())) {
NotificationStatusAudit statusAudit = new NotificationStatusAudit();
statusAudit.setInsertionTime(new Date());
statusAudit.setNewVal("abc");
statusAudit.setOldval("asdf");
statusAudit.setTargetColumnName(targetColumn);
statusAudit.setTargetRecordId((Long) objectChanges.getId());
statusAudit = statusAuditRepo.save(statusAudit);
}
}
}
Here all I have to do is save the record in another (Audit) table when data is getting inserted in one table. My application is spring boot app and am using eclipseLink for persistent. I had to manually register my entity-listener in "PostConstruct" because if it is registered using #EntityListner annotation , spring-data-repos were not getting autowired. Here are my questions :
1) Using EntityListener for my requirement is good approach or should I use direct "save" operations ?
2) I debugged the EntityListener code and method is not initiated a new Transaction even after adding Requires_new. I can see method is not being called $proxy (spring-proxy). I don't understand why ?
I am not sure about what you are doing in your #PostConstruct init() method... but I suspect you should be configuring this DescriptorEventAdapter using EclipseLink's DescriptorCustomizer. Here is an example:
public class MessageEventListener extends DescriptorEventAdapter implements DescriptorCustomizer {
#Override
public void customize(ClassDescriptor descriptor) {
descriptor.getEventManager().addListener(this);
}
#Override
public void postUpdate(DescriptorEvent event) {
ObjectChangeSet objectChanges = ((UpdateObjectQuery) event.getQuery()).getObjectChangeSet();
//More business logic...
}
}
#Entity
#Customizer(MessageEventListener.class)
public class Message {
#Id private long id;
private String content;
}

Spring - Failed to convert property value of type java.lang.String to required type

I am making a project of the Housing Association in Spring.
When I'm trying to add an object to my list of apartments I'm getting an error that is written somehow on the page:
https://s28.postimg.org/vrhy6mbd9/blad.jpg
Apartments have relation Many to One Building.
Apartment Controller:
package pl.dmcs.spoldzielnia.controllers;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.servlet.ModelAndView;
import pl.dmcs.spoldzielnia.domain.Apartment;
import pl.dmcs.spoldzielnia.service.ApartmentService;
import pl.dmcs.spoldzielnia.service.BuildingService;
#Controller
#SessionAttributes
public class ApartmentController {
#Autowired
ApartmentService apartmentService;
#Autowired
BuildingService buildingService;
#RequestMapping("admin/apartment")
public String listApartment(Map<String, Object> map, HttpServletRequest request) {
int apartmentId = ServletRequestUtils.getIntParameter(request, "apartmentId" , -1);
if (apartmentId > 0)
{
Apartment apartment = apartmentService.getApartment(apartmentId);
apartment.setBuilding(buildingService.getBuilding(apartmentService.getApartment(apartmentId).getBuilding().getId()));
map.put("selectedBuilding", apartmentService.getApartment(apartmentId).getBuilding().getId());
map.put("apartment", apartment);
}
else
map.put("apartment", new Apartment());
map.put("buildingList", buildingService.listBuilding());
map.put("apartmentList", apartmentService.listApartment());
return "apartment";
}
#RequestMapping(value = "admin/addApartment", method = RequestMethod.POST)
public String addContact(#ModelAttribute("apartment") Apartment apartment, BindingResult result,
HttpServletRequest request, Map<String, Object> map) {
if (result.getErrorCount()==0)
{
if (apartment.getId()==0)
{
if (apartment.getBuilding().getId() > 0)
apartment.setBuilding(buildingService.getBuilding(apartment.getBuilding().getId()));
apartmentService.addApartment(apartment);
}
else
{
apartmentService.editApartment(apartment);
}
return "redirect:/admin/apartment.html";
}
map.put("buildingList", buildingService.listBuilding());
map.put("apartmentList", apartmentService.listApartment());
return "apartment";
}
#RequestMapping("admin/delete/apartment/{apartmentId}")
public String deleteApartment(#PathVariable("apartmentId") Integer apartmentId) {
apartmentService.removeApartment(apartmentId);
return "redirect:/admin/apartment.html";
}
// #RequestMapping("/apartment")
// public ModelAndView showContacts() {
//
// return new ModelAndView("apartment", "command", new Apartment());
// }
Domain:
package pl.dmcs.spoldzielnia.domain;
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 javax.persistence.Table;
#Entity
#Table(name="apartment")
public class Apartment {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
int id;
#Column(name="apartmentNumber", nullable=false)
private String number;
public String getNumber() {
return number;
}
public void setNumber(String number) {
this.number = number;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
#ManyToOne
private Building building;
public Building getBuilding() {
return building;
}
public void setBuilding(Building building) {
this.building = building;
}
}
}
Building Service Implementation:
package pl.dmcs.spoldzielnia.service;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import pl.dmcs.spoldzielnia.dao.BuildingDAO;
import pl.dmcs.spoldzielnia.domain.Building;
import pl.dmcs.spoldzielnia.domain.Building;
#Service
#Transactional
public class BuildingServiceImpl implements BuildingService{
#Autowired
BuildingDAO buildingDAO;
#Transactional
public void addBuilding(Building building) {
buildingDAO.addBuilding(building);
}
#Transactional
public List<Building> listBuilding() {
return buildingDAO.listBuilding();
}
#Transactional
public Building getBuilding(int id) {
return buildingDAO.getBuilding(id);
}
#Transactional
public void removeBuilding(int id) {
buildingDAO.removeBuilding(id);
}
#Transactional
public void editBuilding(Building building) {
buildingDAO.editBuilding(building);
}
}
Could you help me to solve my problem?
The problem is that you are assuming that Spring MVC is going to be able to fill your Apartment object from the data passed. From the form it looks like the Building number is 12, which probably is a unique identifier for the Building in the database, but how is Spring MVC going to know how to go to the database, retrieve the proper building object and put it into the Apartment object?
Remember that objects mapped through SpringMVC parameters are regular Java POJOs, not Hibernate attached entities. So, when the mapping occurs SpringMVC is trying to put "12" into the building attribute of type Building into your POJO (which explains the error you are getting).
You have two options:
First, you can register a custom formatter, that will use the passed id to retrieve a Building from the database:
import org.springframework.core.convert.converter.Converter;
public class BuildingIdToBuildingConverter implements Converter<String, Building> {
private BuildingService buildingService;
public BuildingIdToBuildingConverter(BuildingService buildingService) {
this.buildingService = buildingService;
}
#Override
public Building convert (String id) {
return buildingService.getBuilding(id);
}
}
And register it:
public class AppConfig extends WebMvcConfigurerAdapter {
...
#Bean
public BuildingService buildingService(){
return new BuildingService();
}
#Override
public void addFormatters (FormatterRegistry registry) {
registry.addConverter(new BuildingIdToBuildingConverter(buildingService()));
}
}
Second, do this work manually by sending the building id in a separate parameter:
#RequestMapping(value = "admin/addApartment", method = RequestMethod.POST)
public String addContact(#ModelAttribute("apartment") Apartment apartment, #RequestParam("buildingId") String buildingId, BindingResult result, HttpServletRequest request, Map<String, Object> map) {
if (result.getErrorCount()==0){
if (apartment.getId()==0){
apartment.setBuilding(buildingService.getBuilding(buildingId));
apartmentService.addApartment(apartment);
}
}
else{
apartmentService.editApartment(apartment);
}
return "redirect:/admin/apartment.html";
}
map.put("buildingList", buildingService.listBuilding());
map.put("apartmentList", apartmentService.listApartment());
return "apartment";
}
And change your HTML accordingly to send the buildingId value.

How to set jersey in spring boot 1.4.1-RELEASE to convert dates to ISO 8601?

In Spring boot 1.3.6-RELEASE I had the below class registered to jersey. Every java.util.Date field would be read and returned as ISO8601 format. However, when updating to 1.4.1-RELEASE it now sometimes works and sometimes doesn't. What's the new proper way to enable this?
package com.mypackage;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.text.ParseException;
import java.text.ParsePosition;
import java.util.Date;
import javax.ws.rs.ext.ParamConverter;
import javax.ws.rs.ext.ParamConverterProvider;
import javax.ws.rs.ext.Provider;
import com.fasterxml.jackson.databind.util.ISO8601Utils;
#Provider
public class DateTimeParamConverterProvider implements ParamConverterProvider {
#SuppressWarnings("unchecked")
#Override
public <T> ParamConverter<T> getConverter(Class<T> clazz, Type type, Annotation[] annotations) {
if (type.equals(Date.class)) {
return (ParamConverter<T>) new DateTimeParamConverter();
} else {
return null;
}
}
static class DateTimeParamConverter implements ParamConverter<Date> {
#Override
public java.util.Date fromString(String value) {
if (value == null) {
return null;
}
try {
return ISO8601Utils.parse(value, new ParsePosition(0));
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
#Override
public String toString(Date value) {
return ISO8601Utils.format(value);
}
}
}
I register this provider like this:
#Component
#ApplicationPath("/")
public class JerseyConfiguration extends ResourceConfig {
private static final Logger log = Logger.getLogger(JerseyConfiguration.class.getName());
#Autowired
public JerseyConfiguration(LogRequestFilter lrf) {
register(new ObjectMapperContextResolverNonNull());
register(RestServiceImpl.class);
property(ServletProperties.FILTER_FORWARD_ON_404, true);
register(DateTimeParamConverterProvider.class, 6000);
...
Just define this in your application.properties:
spring.jackson.date-format=com.fasterxml.jackson.databind.util.ISO8601DateFormat

Resources