I am trying to use WebClient to Post a loan object to another microservice which saves this object in a DB. So theoretically the body (JSON loan object) should just be passed on to the API of the DB service. Somehow, I can't figure out how to accomplish this.
This is the API that accepts the JSON loan object:
Mapping: localhost:8081/loans
#PostMapping
public <T extends Loan> void addLoan(#Valid #NonNull #RequestBody T loan) {
loanService.createLoan(loan);
}
It then calls the loanService which should pass on the loan object to the DB-service API
public <T extends Loan> T createLoan(T loan) {
ParameterizedTypeReference<T> typeReference = new ParameterizedTypeReference<T>(){};
T a = client.post().uri("/loans").body(BodyInserters.fromValue(loan)).retrieve().bodyToMono(typeReference).block();
return a;
}
This is the API of that DB service:
Mapping: localhost:8080/api/v1/loans
#PostMapping
#ResponseBody
public <T extends Loan> T createLoan(#RequestBody T loan) {
return loanService.createLoan(loan);
}
And here is its service:
public <T extends Loan> T createLoan(T Loan) {
return (T) loanRepository.save(Loan);
}
If I just pass a loan object directly to the DB service API, everything works fine. But if I pass it to the other API, I get the following error:
"status": 500,
"error": "Internal Server Error",
"trace": "org.springframework.web.reactive.function.client.WebClientResponseException$InternalServerError: 500 Internal Server Error from POST http://localhost:8080/api/v1/loans/\n\tat org.springframework.web.reactive.function.client.WebClientResponseException.create(WebClientResponseException.java:201)\n\tSuppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: \nError has been observed at the following site(s):\n\t|_ checkpoint ⇢ 500 from POST http://localhost:8080/api/v1/loans/ [DefaultWebClient]\nStack trace:\n\t\tat org.springframework.web.reactive.function.client.WebClientResponseException.create(WebClientResponseException.java:201)\n\t\tat org.springframework.web.reactive.function.client.DefaultClientResponse.lambda$createException$1(DefaultClientResponse.java:216)\n\t\tat reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:106)\n\t\tat reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onNext(FluxOnErrorResume.java:79)\n\t\tat reactor.core.publisher.FluxDefaultIfEmpty$DefaultIfEmptySubscriber.onNext(FluxDefaultIfEmpty.java:99)\n\t\tat reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.java:127)\n\t\tat reactor.core.publisher.FluxContextWrite$ContextWriteSubscriber.onNext(FluxContextWrite.java:107)\n\t\tat reactor.core.publisher.FluxMapFuseable$MapFuseableConditionalSubscriber.onNext(FluxMapFuseable.java:295)\n\t\tat reactor.core.publisher.FluxFilterFuseable$FilterFuseableConditionalSubscriber.onNext(FluxFilterFuseable.java:337)\n\t\tat reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1784)\n\t\tat reactor.core.publisher.MonoCollect$CollectSubscriber.onComplete(MonoCollect.java:159)\n\t\tat reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:142)\n\t\tat reactor.core.publisher.FluxPeek$PeekSubscriber.onComplete(FluxPeek.java:259)\n\t\tat reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:142)\n\t\tat reactor.netty.channel.FluxReceive.onInboundComplete(FluxReceive.java:383)\n\t\tat reactor.netty.channel.ChannelOperations.onInboundComplete(ChannelOperations.java:396)\n\t\tat reactor.netty.channel.ChannelOperations.terminate(ChannelOperations.java:452)\n\t\tat reactor.netty.http.client.HttpClientOperations.onInboundNext(HttpClientOperations.java:664)\n\t\tat reactor.netty.channel.ChannelOperationsHandler.channelRead(ChannelOperationsHandler.java:94)\n\t\tat io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)\n\t\tat io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)\n\t\tat io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)\n\t\tat io.netty.handler.codec.MessageToMessageDecoder.channelRead(MessageToMessageDecoder.java:103)\n\t\tat io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)\n\t\tat io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)\n\t\tat io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)\n\t\tat io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:436)\n\t\tat io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:324)\n\t\tat io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:296)\n\t\tat io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:251)\n\t\tat io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)\n\t\tat io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)\n\t\tat io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)\n\t\tat io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)\n\t\tat io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)\n\t\tat io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)\n\t\tat io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)\n\t\tat io.netty.channel.epoll.AbstractEpollStreamChannel$EpollStreamUnsafe.epollInReady(AbstractEpollStreamChannel.java:795)\n\t\tat io.netty.channel.epoll.EpollEventLoop.processReady(EpollEventLoop.java:480)\n\t\tat io.netty.channel.epoll.EpollEventLoop.run(EpollEventLoop.java:378)\n\t\tat io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989)\n\t\tat io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)\n\t\tat io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)\n\t\tat java.base/java.lang.Thread.run(Thread.java:832)\n\tSuppressed: java.lang.Exception: #block terminated with an error\n\t\tat reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:99)\n\t\tat reactor.core.publisher.Mono.block(Mono.java:1679)\n\t\tat de.rwth.swc.lab.ws2021.daifu.businesslogic.services.LoanService.createLoan(LoanService.java:39)\n\t\tat de.rwth.swc.lab.ws2021.daifu.businesslogic.api.LoanController.addLoan(LoanController.java:28)\n\t\tat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\n\t\tat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:64)\n\t\tat java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\n\t\tat java.base/java.lang.reflect.Method.invoke(Method.java:564)\n\t\tat org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:197)\n\t\tat org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:141)\n\t\tat org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:106)\n\t\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:893)\n\t\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:807)\n\t\tat org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)\n\t\tat org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1061)\n\t\tat org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:961)\n\t\tat org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)\n\t\tat org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909)\n\t\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:652)\n\t\tat org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)\n\t\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:733)\n\t\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)\n\t\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\t\tat org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)\n\t\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\n\t\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\t\tat org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)\n\t\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)\n\t\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\n\t\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\t\tat org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)\n\t\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)\n\t\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\n\t\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\t\tat org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)\n\t\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)\n\t\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\n\t\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\t\tat org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202)\n\t\tat org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)\n\t\tat org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542)\n\t\tat org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:143)\n\t\tat org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)\n\t\tat org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)\n\t\tat org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)\n\t\tat org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:374)\n\t\tat org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)\n\t\tat org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868)\n\t\tat org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1590)\n\t\tat org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)\n\t\tat java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)\n\t\tat java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:630)\n\t\tat org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)\n\t\tat java.base/java.lang.Thread.run(Thread.java:832)\n",
"message": "500 Internal Server Error from POST http://localhost:8080/api/v1/loans/",
"path": "/loans/"
This is the server-side error:
Servlet.service() for servlet [dispatcherServlet] in context with path [/api/v1] threw exception [Request processing failed; nested exception is org.springframework.dao.DataIntegrityViolationException: not-null property references a null or transient value : de.rwth.swc.lab.ws2021.daifu.dataservice.data.models.loans.PrivateLoan.customer; nested exception is org.hibernate.PropertyValueException: not-null property references a null or transient value : de.rwth.swc.lab.ws2021.daifu.dataservice.data.models.loans.PrivateLoan.customer] with root cause org.hibernate.PropertyValueException: not-null property references a null or transient value : de.rwth.swc.lab.ws2021.daifu.dataservice.data.models.loans.PrivateLoan.customer
And finally, this is the POST-body:
{
"amount": 10000.00,
"balance": -2000.00,
"customer": {"id": 1},
"interest": 0.06,
"status": "TIMELY",
"reason": "Some reaseon",
"type": "privateLoan"
}
The error says the "not-null property references a null or transient value" but the exact same request works for a direct POST-request to the 2nd API which doesn't make sense to me.
Here is the loan class:
#Entity
#Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
#Getter
#Setter
#NoArgsConstructor
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
#JsonSubTypes({
#JsonSubTypes.Type(value = CarLoan.class, name = "carLoan"),
#JsonSubTypes.Type(value = ConstructionLoan.class, name = "constructionLoan"),
#JsonSubTypes.Type(value = Mortgage.class, name = "mortgage"),
#JsonSubTypes.Type(value = PrivateLoan.class, name = "privateLoan"),
#JsonSubTypes.Type(value = PropertyLoan.class, name = "propertyLoan")
})
#ApiModel(
discriminator = "type",
subTypes = {CarLoan.class, ConstructionLoan.class, Mortgage.class, PrivateLoan.class, PropertyLoan.class}
)
public abstract class Loan {
#Id
#GeneratedValue(strategy = GenerationType.TABLE)
#Column(name = "id")
#ApiModelProperty(required = false, hidden = true)
protected Integer id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "customer_id", nullable = false)
#JsonBackReference(value = "customer-loans")
protected Customer customer;
#OneToMany(mappedBy = "loan", cascade = CascadeType.ALL)
#JsonManagedReference(value = "loan-loanRates")
private Set<LoanRate> loanRates;
#NonNull
protected Double amount;
#NonNull
protected Double interest;
#NonNull
protected Double balance;
#NonNull
protected LoanStatus status;
public enum LoanStatus {
TIMELY("timely"),
GRACE_PERIOD("grace period"),
DEFAULT("default"),
DEFICIT("deficit"),
IRRECOVERABLE_DEBT("irrecoverable debt"),
CLOSED("closed");
#Getter
private String stringRepresentation;
private LoanStatus(String s) {
this.stringRepresentation = s;
}
}
public <T extends Loan> boolean isOfSameInstance(T otherLoan) {
return (this.getClass().equals(otherLoan.getClass()));
}
}
Let me know if I should post anything else.
Thanks in advance.
The problem is due to the models being used in the projects. As your are reusing the model classes of the one webservice which offers the CRUD api for the backend, you are also reusing the jackson's #JsonManagedReference and #JsonBackReference. This leads to null values for the models being defined as the back reference, such as the customer in you loan class. Jackson does not serialize such tagged objects to JSON in order to not run into a stackoverflow due to infinite recursion. Thus, when you serialize a loan model in your service and send the request to the other service, jackson nulls the back reference, e.g. customer in the loan model and the 2nd webservice therefore receives an invalid loan model, since a loan model is required to have a customer not to be null.
I suggest to either remove the jackson annotations from the models in the service you develop, which would required copy pasted model classes (on the one side the classes using the required jackson annotations in the web service, and on the other side the classes not using these in the other web service). However, this solution has the typical disadvantages of duplicated code. The more elegant but more complicated solution will be to implement a custom jackson serializer and deserializer by specializing jackson's StdSerializer<Loan> and StdDeserializer<Loan>. These custom serializers and deserializers should override its serialize(T value, JsonGenerator gen, SerializerProvider provider) respectively its deserialize(JsonParser, DeserializationContext) method such that the #JsonManagedReference and #JsonBackReference, as well as, if being used, #JsonIgnore annotations in the model are being ignored.
It might be sufficient to just implement a custom serializer. However, I guess that you will also run into problems when receiving a response from the other web service when not using a custom deserializer.
This error may happen if the customer object in the loan you are trying to save is null or not yet added to the database (even though it is set in loan). You should check before saving the Loan in the DB if customer is null or not. If not, and if it is a customer not yet in the database, you should consider adding it first or specify CascadeType.PERSIST in the relation type annotation. In any case, it would be better if you post the entire model that both services are using.
Related
I'm making and simple application in Kotlin using Spring but I'm having a problem with the validation.
I have this entity class:
#Entity
#Table(name = "category")
data class Category(
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long?,
#field:NotNull #field:NotEmpty val name: String)
And my controller function like this:
#PostMapping
#ResponseStatus(HttpStatus.CREATED)
fun create(#Valid #RequestBody category: Category): ResponseEntity<Category>
create have some code, but it is irrelevant for the question, my problem is with the request body validation. If I send a category with an empty name field, it is thrown a MethodArgumentNotValidException exception, but if I send null to the field name, the exception thrown HttpMessageNotReadableException instead. Does anyone knows if it is possible to make passing null to a field marked with #NotNull to also throw MethodArgumentNotValidException in Kotlin.
So your problem is you specify the name field as not nullable, by default jackson module for kotlin will check it and throw HttpMessageNotReadableException which cause by MissingKotlinParameterException during json mapping process. If you mark name filed as nullable json mapping will passed and get to the spring validation phase with #Valid then we will get MethodArgumentNotValidException
#Entity
#Table(name = "category")
data class Category(
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long?,
#field:NotNull #field:NotEmpty val name: String?)
You can handle this issue by providing HttpMessageNotReadableException handler
and then checking if the underlying cause is MissingKotlinParameterException.
After that, you can provide custom validation error. I'm using zalando-problem, so syntax is a bit different from vanilla spring, but you get the idea:
#ExceptionHandler
override fun handleMessageNotReadableException(
exception: HttpMessageNotReadableException,
request: NativeWebRequest
): ResponseEntity<Problem> {
// workaround
val cause = exception.cause
if (cause is MissingKotlinParameterException) {
val violations = setOf(createMissingKotlinParameterViolation(cause))
return newConstraintViolationProblem(exception, violations, request)
}
return create(Status.BAD_REQUEST, UnableToReadInputMessageProblem(), request)
}
private fun createMissingKotlinParameterViolation(cause: MissingKotlinParameterException): Violation {
val name = cause.path.fold("") { jsonPath, ref ->
val suffix = when {
ref.index > -1 -> "[${ref.index}]"
else -> ".${ref.fieldName}"
}
(jsonPath + suffix).removePrefix(".")
}
return Violation(name, "must not be null")
}
This way you get get nice output with proper constraint error.
You may try to declare #ExceptionHandler for MissingKotlinParameterException directly (though I've tried, but it didn't some reason), but I can't guarantee it'll work.
Code samples for path parsing are taken from here
I've a project with Spring Boot 1.5.7, Spring Data REST, Hibernate, Spring JPA, Swagger2.
I've two beans like these:
#Entity
public class TicketBundle extends AbstractEntity {
private static final long serialVersionUID = 404514926837058071L;
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Note> notes = new ArrayList<>();
.....
}
and
#Entity
public class Note extends AbstractEntity {
private static final long serialVersionUID = -5062313842902549565L;
#Lob
private String text;
...
}
I'm exposing my methods via Repository:
#Transactional
#RepositoryRestResource(excerptProjection = TicketBundleProjection.class)
#PreAuthorize("isAuthenticated()")
public interface TicketBundleRepository extends PagingAndSortingRepository<TicketBundle, Long> {
....
}
so in swagger I see the endpoint in which I'm interested that is needed to load the collection of notes from a specific ticket bundle:
Now, I want to override the default GET /api/v1/ticketBundles/{id}/notes and replace that with my custom method I put in TicketBundleRepository:
#Transactional(readOnly = true)
#RestResource(rel = "ticketBundleNotes", path = "/ticketBundles/{id}/notes")
#RequestMapping(method = RequestMethod.GET, path = "/ticketBundles/{id}/notes")
#Query("SELECT n FROM TicketBundle tb JOIN tb.notes n WHERE tb.id=:id ORDER BY n.createdDate DESC,n.id DESC")
public Page<Note> getNotes(#Param("id") long id, Pageable pageable);
It's very convenient create the query in this way because I need to use Pageable and return a Page. Unfortunately I've two problems at this point.
First problem
The method is mapped on the endpoint /api/v1/ticketBundles/search/ticketBundles/{id}/notes instad of /api/v1/ticketBundles/ticketBundles/{id}/notes
Second problem
When I call the method from swagger I receive an HTTP 404:
The request seems wrong. Seems the path variable is not understood:
curl -X GET --header 'Accept: application/json' 'http://localhost:8080/api/v1/ticketBundles/search/ticketBundles/{id}/notes?id=1'
This is the response from the server:
{
"timestamp": "2017-10-05T14:00:35.563+0000",
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/api/v1/ticketBundles/search/ticketBundles/%7Bid%7D/notes"
}
without any error on the server side.
Is there a way to override the endpoint GET/api/v1/ticketBundles/{id}/notes exposing it through Repository without using a custom controller (using that I would loose the facilities to manage the Pageable)?
Furthermore, what am I doing wrong to get a HTTP 404 in the call I shown above?
I believe you are using incorrect annotations. You would need to annotate your class with #RestController and use #PathVariable on your method instead of #Param. Here is a working sample, you may want to tailor it according to your needs.
#org.springframework.data.rest.webmvc.RepositoryRestController
#org.springframework.web.bind.annotation.RestController
public interface PersonRepository extends org.springframework.data.repository.PagingAndSortingRepository<Person, Long> {
#org.springframework.web.bind.annotation.GetMapping(path = "/people/{id}")
Person findById(#org.springframework.web.bind.annotation.PathVariable("id") Long id);
}
In my project I have 2 entities. Survey and entries to survey. They are in relation one to many (thare can be many entries to one survey).
#NoArgsConstructor
#AllArgsConstructor
#Getter
#Setter
#Entity
#Table(name = "survey_entries")
#TypeDef(name = "SurveyEntry", typeClass = SurveyEntry.class)
public class SurveyEntryEntity extends AbstractEntity {
#ManyToOne
#JoinColumn(name = "survey_id")
private SurveyEntity survey;
#NonNull
#Type(type = "SurveyEntry")
#Column(name = "responses")
// JSON db column type mapped to custom type
private SurveyEntry responses;
}
#NoArgsConstructor
#AllArgsConstructor
#Getter
#Setter
#Entity
#Table(name = "surveys")
#TypeDef(name = "Survey", typeClass = Survey.class)
public class SurveyEntity extends AbstractEntity {
#NonNull
#Type(type = "Survey")
#Column(name = "template")
// JSON db column type mapped to custom type
private Survey survey;
#OneToMany(mappedBy = "survey")
private List<SurveyEntryEntity> entries;
}
I have also created 2 rest repositories using Spring Data Rest:
#RepositoryRestResource(collectionResourceRel = "survey_entries", path = "survey-entries")
public interface SurveyEntryRepository extends PagingAndSortingRepository<SurveyEntryEntity, Long> {
}
#RepositoryRestResource(collectionResourceRel = "surveys", path = "surveys")
public interface SurveyRepository extends PagingAndSortingRepository<SurveyEntity,Long> {
}
I have successfully added survey by rest POST request and I can access it entries (currently empty) by sending GET to /api/surveys/1/entries.Now I want to add entry to exisiting survey. And while I can add it by sending POST (content below) to /api/survey-entries I have troubles adding it directly as a reference to survey. I'm using POST method with the same content and url /api/surveys/1/entries. What is interesting, I'm getting NullPointerException in logs and entry is not inserted but audit modify timestamp in survey is changed. What am I doing wrong? Did I miss same configuration? Or should I use different content?
Content of POST with entry:
{
"responses": {
"question1": "response1",
"question2": "response2",
"question3": "response3"
}
}
Content of POST with survey:
{
"survey": {
//survey structure
}
}
Exception:
08:41:14.730 [http-nio-8080-exec-3] DEBUG org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod - Failed to resolve argument 1 of type 'org.springframework.data.rest.webmvc.PersistentEntityResource'
org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: No content to map due to end-of-input; nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: No content to map due to end-of-input
Caused by: com.fasterxml.jackson.databind.exc.MismatchedInputException: No content to map due to end-of-input
#EDIT
I have tried adding entry by POST to /api/survey-entries with 'application/hal+json' Content-Type header and content as below, but now I'm getting other exception:
Content:
{
"survey" : "http://localhost:8080/api/surveys/1",
"responses": {
"question1": "response1",
"question2": "response2",
"question3": "response3"
}
}
Exception:
Caused by: com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `com.domain.SurveyEntity` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('http://localhost:8080/api/surveys/1')
at [Source: (org.apache.catalina.connector.CoyoteInputStream); line: 1, column: 41] (through reference chain: com.domain.SurveyEntryEntity["survey"])
#Edit 2
Added Lombok annotations present on Entity classess
Unfortunatelly problem lied in Lombok annotations which weren't included in sample code. I added them now so any one can see where the problem lies.
I managed to solve it by downgrading Lombok to version (1.16.14) and changing annotation #AllArgsConstructor to #AllArgsConstructor(suppressConstructorProperties = true). It's immposible to achieve in later Lombok versions as this property is currently removed.
I have found solution on Spring Data Rest JIRA. There is already issue DATAREST-884 mentioning problem and presenting solution/workaround.
Sorry for wasted time while it was impossible to see solution without all the code.
I have a database with one table named person:
id | first_name | last_name | date_of_birth
----|------------|-----------|---------------
1 | Tin | Tin | 2000-10-10
There's a JPA entity named Person that maps to this table:
#Entity
#XmlRootElement(name = "person")
#XmlAccessorType(NONE)
public class Person {
#Id
#GeneratedValue
private Long id;
#XmlAttribute(name = "id")
private Long externalId;
#XmlAttribute(name = "first-name")
private String firstName;
#XmlAttribute(name = "last-name")
private String lastName;
#XmlAttribute(name = "dob")
private String dateOfBirth;
// setters and getters
}
The entity is also annotated with JAXB annotations to allow XML payload in
HTTP requests to be mapped to instances of the entity.
I want to implement an endpoint for retrieving and updating an entity with a given id.
According to this answer to a similar question,
all I need to do is to implement the handler method as follows:
#RestController
#RequestMapping(
path = "/persons",
consumes = APPLICATION_XML_VALUE,
produces = APPLICATION_XML_VALUE
)
public class PersonController {
private final PersonRepository personRepository;
#Autowired
public PersonController(final PersonRepository personRepository) {
this.personRepository = personRepository;
}
#PutMapping(value = "/{person}")
public Person savePerson(#ModelAttribute Person person) {
return personRepository.save(person);
}
}
However this is not working as expected as can be verified by the following failing test case:
#RunWith(SpringRunner.class)
#SpringBootTest(webEnvironment = RANDOM_PORT)
public class PersonControllerTest {
#Autowired
private TestRestTemplate restTemplate;
private HttpHeaders headers;
#Before
public void before() {
headers = new HttpHeaders();
headers.setContentType(APPLICATION_XML);
}
// Test fails
#Test
#DirtiesContext
public void testSavePerson() {
final HttpEntity<Object> request = new HttpEntity<>("<person first-name=\"Tin Tin\" last-name=\"Herge\" dob=\"1907-05-22\"></person>", headers);
final ResponseEntity<Person> response = restTemplate.exchange("/persons/1", PUT, request, Person.class, "1");
assertThat(response.getStatusCode(), equalTo(OK));
final Person body = response.getBody();
assertThat(body.getFirstName(), equalTo("Tin Tin")); // Fails
assertThat(body.getLastName(), equalTo("Herge"));
assertThat(body.getDateOfBirth(), equalTo("1907-05-22"));
}
}
The first assertion fails with:
java.lang.AssertionError:
Expected: "Tin Tin"
but: was "Tin"
Expected :Tin Tin
Actual :Tin
In other words:
No server-side exceptions occur (status code is 200)
Spring successfully loads the Person instance with id=1
But its properties do not get updated
Any ideas what am I missing here?
Note 1
The solution provided here is not working.
Note 2
Full working code that demonstrates the problem is provided
here.
More Details
Expected behavior:
Load the Person instance with id=1
Populate the properties of the loaded person entity with the XML payload using Jaxb2RootElementHttpMessageConverter or MappingJackson2XmlHttpMessageConverter
Hand it to the controller's action handler as its person argument
Actual behavior:
The Person instance with id=1 is loaded
The instance's properties are not updated to match the XML in the request payload
Properties of the person instance handed to the controller's action handler method are not updated
this '#PutMapping(value = "/{person}")' brings some magic, because {person} in your case is just '1', but it happens to load it from database and put to ModelAttribute in controller. Whatever you change in test ( it can be even empty) spring will load person from database ( effectively ignoring your input ), you can stop with debugger at the very first line of controller to verify it.
You can work with it this way:
#PutMapping(value = "/{id}")
public Person savePerson(#RequestBody Person person, #PathVariable("id") Long id ) {
Person found = personRepository.findOne(id);
//merge 'found' from database with send person, or just send it with id
//Person merged..
return personRepository.save(merged);
}
wrong mapping in controller
to update entity you need to get it in persisted (managed) state first, then copy desired state on it.
consider introducing DTO for your bussiness objects, as, later, responding with persisted state entities could cause troubles (e.g. undesired lazy collections fetching or entities relations serialization to XML, JSON could cause stackoverflow due to infinite method calls)
Below is simple case of fixing your test:
#PutMapping(value = "/{id}")
public Person savePerson(#PathVariable Long id, #RequestBody Person person) {
Person persisted = personRepository.findOne(id);
if (persisted != null) {
persisted.setFirstName(person.getFirstName());
persisted.setLastName(person.getLastName());
persisted.setDateOfBirth(person.getDateOfBirth());
return persisted;
} else {
return personRepository.save(person);
}
}
Update
#PutMapping(value = "/{person}")
public Person savePerson(#ModelAttribute Person person, #RequestBody Person req) {
person.setFirstName(req.getFirstName());
person.setLastName(req.getLastName());
person.setDateOfBirth(req.getDateOfBirth());
return person;
}
The issue is that when you call personRepository.save(person) your person entity does not have the primary key field(id) and so the database ends up having two records with the new records primary key being generated by the db. The fix will be to create a setter for your id field and use it to set the entity's id before saving it:
#PutMapping(value = "/{id}")
public Person savePerson(#RequestBody Person person, #PathVariable("id") Long id) {
person.setId(id);
return personRepository.save(person);
}
Also, like has been suggested by #freakman you should use #RequestBody to capture the raw json/xml and transform it to a domain model. Also, if you don't want to create a setter for your primary key field, another option may be to support an update operation based on any other unique field (like externalId) and call that instead.
For updating any entity the load and save must be in same Transaction,else it will create new one on save() call,or will throw duplicate primary key constraint violation Exception.
To update any we need to put entity ,load()/find() and save() in same transaction, or write JPQL UPDATE query in #Repository class,and annotate that method with #Modifying .
#Modifying annotation will not fire additional select query to load entity object to update it,rather presumes that there must be a record in DB with input pk,which needs to update.
I'm using Spring 3.1.0.RELEASE with Hibernate 4.0.1.Final. I want to invoke a search method in a controller that takes as input a search bean (the Event bean below) ...
#RequestMapping(value = "/search_results.jsp")
public ModelAndView processSearch(final HttpServletRequest request, final Event searchBean, final BindingResult result) {
...
}
The event bean contains the following field ...
#Entity
#Table(name = "EVENTS")
public class Event implements Comparable {
...
#OneToOne(cascade = CascadeType.ALL)
#JoinColumn(name="EVENT_FEED_ID")
private EventFeed eventFeed;
...
}
in which the EventFeed object contains the following fields ...
#Entity
#Table(name = "EVENT_FEEDS")
public class EventFeed {
#Id
#Column(name = "ID")
#GeneratedValue(strategy=GenerationType.AUTO)
private Integer id;
#NotEmpty
#Column(name = "TITLE")
private String title;
...
}
How do I construct a URL such that the search bean's Event.getEventFeed().getId() field is populated?
I realize I could submit a GET request with a parameter like "eventFeedId=2" and populate everything manually, but since other pages are submitting requests that populate the command object, I'd like to continue to use the same logic.
It would be
/search_results.jsp?event.eventFeed.id=...&event.eventFeed.title=...
event is a default model attribute name as defined in #ModelAttribute, other binding rules are described in 5.4.1 Setting and getting basic and nested properties.
Note, however, that this approach can cause problems if you'll associate these bean with Hibernate session later. For example, if you want to attach new Event to the existing EventFeed by calling merge() it would also override the title property. Thus, in such a case it would be better to avoid overuse of data binding and pass primitives as parameters instead.