Spring REST: Appropriate constructor for nested XML request body? - java

I have a REST callback service that must consume XML in the following format:
<SearchRequest>
<SearchCriteria>
<Param1></Param2>
<Param2></Param2>
</SearchCriteria>
</SearchRequest>
The actual XML has about 32 parameters within "Criteria", but this gives the basic idea.
I created a SearchRequest class that has the attribute, searchCriteria, and a SearchCriteria class that has param1 and param2.
My REST controller class looks something like this:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
#RestController
#RequestMapping("/acme/request/search")
public class AcmeCallbackController {
#RequestMapping(method = RequestMethod.POST, consumes = "application/xml")
public ResponseEntity<String> postAcmeSearch(#RequestBody SearchRequest body) {
StringBuffer resultBuffer = new StringBuffer(2048);
// implementation code here, 'body' now expected to be a SearchRequest object contructed from request body XML
return new ResponseEntity<String>(resultBuffer.toString(), HttpStatus.OK);
}
When I test the above service I receive the following error response:
`{ "timestamp": 1514390248822,
"status": 400,
"error": "Bad Request",
"exception": org.springframework.http.converter.HttpMessageNotReadableException",
"message": "JSON parse error: Can not construct instance of SearchRequest: no suitable constructor found, can not deserialize from Object value (missing default constructor or creator, or perhaps need to add/enable type information?); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Can not construct instance of SearchRequest: no suitable constructor found, can not deserialize from Object value (missing default constructor or creator, or perhaps need to add/enable type information?)\n at [Source: java.io.PushbackInputStream#26bd9717; line: 2, column: 3]",
"path": "/acme/request/search" }`
Does anyone know the suitable constructor and / or annotations to apply to SearchRequest, so that the xml request is correctly deserialized? I have #JsonProperty("{Attribute}") on all the getters and setters, where {Attribute} is the name of the attribute with an initial cap to match the XML element name, and a constructor that has an argument for each attribute value.
TIA,
Ed

I figured it out. I had to add annotations to the constructor arguments, e.g.
public SearchRequest(#JsonProperty("Param1") String param1,
#JsonProperty("Param2") String param2) {
this.param1 = param1;
this.param2 = param2;
}
It worked fine after that.

Related

How can I instruct io.swagger.v3 to allow posting of a JSON payload together with multipart/form-data?

My current controller code looks like this:
import org.springframework.web.bind.annotation.PostMapping;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
#Slf4j
#RestController
#RequestMapping("/product")
#Tag(name = "Manage products endpoints")
public class ProductController {
#Autowired
ProductsService productsService;
#Operation(summary = "Create product entry.", description = "Create product entry.")
#PostMapping(value = "/", consumes = { "multipart/form-data"})
#RequiresLogin
public ProductDTO createProduct(#RequestParam("file") MultipartFile file, ProductDTO product,
#RequestAttribute(AuthorizationService.SESSION_USERNAME) String sessionUsername) throws IOException {
return productsService.createProduct(file, product, sessionUsername);
}
......
The automatically generated swagger page looks like this:
The post works, is just that the object JSON payload is being passed in the URL as query fields. This is a problem for me as [ and ] chars are being used to define sub objects, causing my app server instance to reject such requests.
dleware_1 | java.lang.IllegalArgumentException: Invalid character found in the request target [/product/?universalProductCode=12345&brand=Nike&mass[value]=250&mass[unit]=GRAM&dimensions[length]=120&dimensions[width]=30&dimensions[unit]=MILLIMETER&category=FOOD&tags=food&tags=eat ]. The valid characters are defined in RFC 7230 and RFC 3986
middleware_1 | at org.apache.coyote.http11.Http11InputBuffer.parseRequestLine(Http11InputBuffer.java:494)
A workaround could be to restructure my object so that no nested objects are defined, but I would much prefer to post that payload as a JSON instead.
Is there a way to instruct io.swagger.v3 to allow posting of a JSON payload together with multipart/form-data ?
Thanks

Javax validation does not include field name

We have built an API in which in our model We have a field
#Min(1) #Max(16)
private Long demoField;
When we provide 17 to demoField
it will throw us an error on the client-side
"must be less than or equal to 16"
But when we see the violation message it includes the field name and the message looks like
"demoField: must be less than or equal to 16"
So the question of why we are not getting field name in the client-side error message.
Am I missing something?
API built on spring boot
It's not passed by default. You could implement your own error handler to customize the message passed back, by using #ControllerAdvice for example.
One way is to just specify the message:
#Min(value = 5, message="Age must be at least 5")
In which case in the #ControllerAdvice, you would just need to read getDefaultMessage()
If you don't want to manually add default messages, the approach would be to implement something along the lines of (with appropriate null checks etc):
package com.example.demo;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
#ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
#Override
protected ResponseEntity<Object> handleBindException(
BindException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
return new ResponseEntity<>(
ex.getFieldError().getField() + ": " + ex.getFieldError().getDefaultMessage(),
headers,
status);
}
}

How do I pass a ZonedDateTime into a Jersey #QueryParam? [duplicate]

I have the following GET REST method:
import java.time.OffsetDateTime;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import com.product.rest.api.TransactionsApi;
import com.product.rest.model.Transaction;
#Path("/transactions")
#Api(description = "the transactions API")
#Consumes({ "application/json" })
#Produces({ "application/json" })
public class TransactionsApiImpl extends TransactionsApi {
#GET
#Consumes({ "application/json" })
#Produces({ "application/json" })
#ApiOperation(value = "", notes = "Get all transactions", response = Transaction.class, responseContainer = "List", tags = {})
#ApiResponses(
value = { #ApiResponse(code = 200, message = "OK", response = Transaction.class, responseContainer = "List"),
#ApiResponse(code = 400, message = "Bad Request", response = Transaction.class, responseContainer = "List"),
#ApiResponse(code = 404, message = "Not Found", response = Transaction.class, responseContainer = "List"),
#ApiResponse(code = 500, message = "Internal Server Error", response = Transaction.class, responseContainer = "List") })
#Override
public Response transactionsGet(
#HeaderParam("tok") String tok,
#QueryParam("param1") Integer param1,
#QueryParam("param2") String param2,
#QueryParam("param3") OffsetDateTime param3,
#QueryParam("param4") OffsetDateTime param4,
#QueryParam("param5") Integer param5,
#QueryParam("param6") Integer param6,
#QueryParam("param7") String param7) {
return Response.ok().entity("Success!").build();
}
The TransactionsApi is a generated implementation using Swagger Codegen, as is the Transaction model class. I have several other functions in this class, but whenever I leave the GET /transactions function uncommented, I receive the following error:
WARN [Thread-1] (ContextHandler.java:2175) - unavailable
org.glassfish.jersey.server.model.ModelValidationException: Validation of the application resource model has failed during application initialization.
[[FATAL] No injection source found for a parameter of type public javax.ws.rs.core.Response
com.product.rest.impl.v1.TransactionsApiImpl.transactionsGet(java.lang.String,java.lang.Integer,java.lang.String,java.time.OffsetDateTime,java.time.OffsetDateTime,java.lang.Integer,java.lang.Integer,java.lang.String) at index 3.; source='ResourceMethod{httpMethod=GET, consumedTypes=[application/json], producedTypes=[application/json], suspended=false, suspendTimeout=0, suspendTimeoutUnit=MILLISECONDS, invocable=Invocable{handler=ClassBasedMethodHandler{handlerClass=class com.product.rest.impl.v1.TransactionsApiImpl, handlerConstructors=[org.glassfish.jersey.server.model.HandlerConstructor#7df78e88]}, definitionMethod=public javax.ws.rs.core.Response
All other similar questions I have found had to do with MultiPart Data and file uploading, whereas I am making a simple GET request. Other functions that also use the javax.ws.rs.code.Response class do not have this issue and the server starts normally.
I have noticed that the problem happens whenever the OffsetDateTime class is in the parameters (i.e. param3 and param4), but I have been unable to find out why. Moreover, OffsetDateTime was chosen by Swagger Codegen and I am reluctant to change it seeing how I will have to change every derived file afterwards whenever I regenerate my sources.
Has anyone had this issue before with REST services and OffsetDateTime?
All other similar questions I have found had to do with MultiPart Data and file uploading
It's related. The error is a general error you get when Jersey can't validate the resource model. Part of the resource model is the method parameters. Jersey has a system for knowing which parameters it will be able to process and which ones it won't. In your case, it doesn't know how to process the OffsetDateTime.
There are a set of rules that you need to follow in order to able to use non basic types as #QueryParams (and all other #XxxParams such as #PathParam and #FormParam, etc.):
Be a primitive type
Have a constructor that accepts a single String argument
Have a static method named valueOf or fromString that accepts a single String argument (see, for example, Integer.valueOf(String))
Have a registered implementation of ParamConverterProvider JAX-RS extension SPI that returns a ParamConverter instance capable of a "from string" conversion for the type.
Be List<T>, Set<T> or SortedSet<T>, where T satisfies 2, 3 or 4 above. The resulting collection is read-only.
So in this case of OffsetDateTime, going down the list; it's not a primitive; it doesn't have a String constructor; it doesn't have a static valueOf or fromString
So basically, the only option left is to implement a ParamConverter/ParamConverterProvider for it. The basic set up looks like
#Provider
public class OffsetDateTimeProvider implements ParamConverterProvider {
#Override
public <T> ParamConverter<T> getConverter(Class<T> clazz, Type type, Annotation[] annotations) {
if (clazz.getName().equals(OffsetDateTime.class.getName())) {
return new ParamConverter<T>() {
#SuppressWarnings("unchecked")
#Override
public T fromString(String value) {
OffsetDateTime time = ...
return (T) time;
}
#Override
public String toString(T time) {
return ...;
}
};
}
return null;
}
}
Jersey will pass you the String value of the query parameter, and it's your job to to create it and return it.
Then just register the OffsetDateTimeProvider with the application. If you're using package scanning, it should be picked up and registered automatically from the #Provider annotation.
I don't use Swagger, so I don't know if they already provide something like this already implemented, but it seems odd that they would generate this for you, and not have a way to make it work. I know Jersey 3 will have Java 8 support out the box, but who know when the heck that's gonna be released.

OffsetDateTime yielding "No injection source found for a parameter of type public javax.ws.rs.core.response" in GET method

I have the following GET REST method:
import java.time.OffsetDateTime;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import com.product.rest.api.TransactionsApi;
import com.product.rest.model.Transaction;
#Path("/transactions")
#Api(description = "the transactions API")
#Consumes({ "application/json" })
#Produces({ "application/json" })
public class TransactionsApiImpl extends TransactionsApi {
#GET
#Consumes({ "application/json" })
#Produces({ "application/json" })
#ApiOperation(value = "", notes = "Get all transactions", response = Transaction.class, responseContainer = "List", tags = {})
#ApiResponses(
value = { #ApiResponse(code = 200, message = "OK", response = Transaction.class, responseContainer = "List"),
#ApiResponse(code = 400, message = "Bad Request", response = Transaction.class, responseContainer = "List"),
#ApiResponse(code = 404, message = "Not Found", response = Transaction.class, responseContainer = "List"),
#ApiResponse(code = 500, message = "Internal Server Error", response = Transaction.class, responseContainer = "List") })
#Override
public Response transactionsGet(
#HeaderParam("tok") String tok,
#QueryParam("param1") Integer param1,
#QueryParam("param2") String param2,
#QueryParam("param3") OffsetDateTime param3,
#QueryParam("param4") OffsetDateTime param4,
#QueryParam("param5") Integer param5,
#QueryParam("param6") Integer param6,
#QueryParam("param7") String param7) {
return Response.ok().entity("Success!").build();
}
The TransactionsApi is a generated implementation using Swagger Codegen, as is the Transaction model class. I have several other functions in this class, but whenever I leave the GET /transactions function uncommented, I receive the following error:
WARN [Thread-1] (ContextHandler.java:2175) - unavailable
org.glassfish.jersey.server.model.ModelValidationException: Validation of the application resource model has failed during application initialization.
[[FATAL] No injection source found for a parameter of type public javax.ws.rs.core.Response
com.product.rest.impl.v1.TransactionsApiImpl.transactionsGet(java.lang.String,java.lang.Integer,java.lang.String,java.time.OffsetDateTime,java.time.OffsetDateTime,java.lang.Integer,java.lang.Integer,java.lang.String) at index 3.; source='ResourceMethod{httpMethod=GET, consumedTypes=[application/json], producedTypes=[application/json], suspended=false, suspendTimeout=0, suspendTimeoutUnit=MILLISECONDS, invocable=Invocable{handler=ClassBasedMethodHandler{handlerClass=class com.product.rest.impl.v1.TransactionsApiImpl, handlerConstructors=[org.glassfish.jersey.server.model.HandlerConstructor#7df78e88]}, definitionMethod=public javax.ws.rs.core.Response
All other similar questions I have found had to do with MultiPart Data and file uploading, whereas I am making a simple GET request. Other functions that also use the javax.ws.rs.code.Response class do not have this issue and the server starts normally.
I have noticed that the problem happens whenever the OffsetDateTime class is in the parameters (i.e. param3 and param4), but I have been unable to find out why. Moreover, OffsetDateTime was chosen by Swagger Codegen and I am reluctant to change it seeing how I will have to change every derived file afterwards whenever I regenerate my sources.
Has anyone had this issue before with REST services and OffsetDateTime?
All other similar questions I have found had to do with MultiPart Data and file uploading
It's related. The error is a general error you get when Jersey can't validate the resource model. Part of the resource model is the method parameters. Jersey has a system for knowing which parameters it will be able to process and which ones it won't. In your case, it doesn't know how to process the OffsetDateTime.
There are a set of rules that you need to follow in order to able to use non basic types as #QueryParams (and all other #XxxParams such as #PathParam and #FormParam, etc.):
Be a primitive type
Have a constructor that accepts a single String argument
Have a static method named valueOf or fromString that accepts a single String argument (see, for example, Integer.valueOf(String))
Have a registered implementation of ParamConverterProvider JAX-RS extension SPI that returns a ParamConverter instance capable of a "from string" conversion for the type.
Be List<T>, Set<T> or SortedSet<T>, where T satisfies 2, 3 or 4 above. The resulting collection is read-only.
So in this case of OffsetDateTime, going down the list; it's not a primitive; it doesn't have a String constructor; it doesn't have a static valueOf or fromString
So basically, the only option left is to implement a ParamConverter/ParamConverterProvider for it. The basic set up looks like
#Provider
public class OffsetDateTimeProvider implements ParamConverterProvider {
#Override
public <T> ParamConverter<T> getConverter(Class<T> clazz, Type type, Annotation[] annotations) {
if (clazz.getName().equals(OffsetDateTime.class.getName())) {
return new ParamConverter<T>() {
#SuppressWarnings("unchecked")
#Override
public T fromString(String value) {
OffsetDateTime time = ...
return (T) time;
}
#Override
public String toString(T time) {
return ...;
}
};
}
return null;
}
}
Jersey will pass you the String value of the query parameter, and it's your job to to create it and return it.
Then just register the OffsetDateTimeProvider with the application. If you're using package scanning, it should be picked up and registered automatically from the #Provider annotation.
I don't use Swagger, so I don't know if they already provide something like this already implemented, but it seems odd that they would generate this for you, and not have a way to make it work. I know Jersey 3 will have Java 8 support out the box, but who know when the heck that's gonna be released.

Spring Data Rest: RepositoryRestController deserialization from URI not working

My Problem is the deserialization of entities from URI-String.
When I use the by Spring Data Rest generated HTTP interface everything works fine.
I can post the following JSON against my endpoint /api/shoppingLists and it will be deserialized to a shopping list with admin as an owner.
{
"name":"Test",
"owners":["http://localhost:8080/api/sLUsers/admin"]
}
When I use a custom RepositoryRestController this doesn't work anymore. If I post exactly the same JSON to the same endpoint I get this response.
{
"timestamp" : "2015-11-15T13:18:34.550+0000",
"status" : 400,
"error" : "Bad Request",
"exception" : "org.springframework.http.converter.HttpMessageNotReadableException",
"message" : "Could not read document: Can not instantiate value of type [simple type, class de.yannicklem.shoppinglist.core.user.entity.SLUser] from String value ('http://localhost:8080/api/sLUsers/admin'); no single-String constructor/factory method\n at [Source: java.io.PushbackInputStream#9cad4d2; line: 1, column: 26] (through reference chain: de.yannicklem.shoppinglist.core.list.entity.ShoppingList[\"owners\"]->java.util.HashSet[0]); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Can not instantiate value of type [simple type, class de.yannicklem.shoppinglist.core.user.entity.SLUser] from String value ('http://localhost:8080/api/sLUsers/admin'); no single-String constructor/factory method\n at [Source: java.io.PushbackInputStream#9cad4d2; line: 1, column: 26] (through reference chain: de.yannicklem.shoppinglist.core.list.entity.ShoppingList[\"owners\"]->java.util.HashSet[0])",
"path" : "/api/shoppingLists"
}
My RepositoryRestController:
#RepositoryRestController
#ExposesResourceFor(ShoppingList.class)
#RequiredArgsConstructor(onConstructor = #__(#Autowired ))
public class ShoppingListRepositoryRestController {
private final ShoppingListService shoppingListService;
private final CurrentUserService currentUserService;
#RequestMapping(method = RequestMethod.POST, value = ShoppingListEndpoints.SHOPPING_LISTS_ENDPOINT)
#ResponseBody
#ResponseStatus(HttpStatus.CREATED)
public PersistentEntityResource postShoppingList(#RequestBody ShoppingList shoppingList,
PersistentEntityResourceAssembler resourceAssembler) {
if (shoppingListService.exists(shoppingList)) {
shoppingListService.handleBeforeSave(shoppingList);
} else {
shoppingListService.handleBeforeCreate(shoppingList);
}
return resourceAssembler.toResource(shoppingListService.save(shoppingList));
}
}
Could anybody tell me why the deserialization doesn't work anymore with a custom RepositoryRestController (which is suggested by the docs)?
To take advantage of Spring Data REST’s settings, message converters, exception handling, and more, use the #RepositoryRestController annotation instead of a standard Spring MVC #Controller or #RestController
For the full source code please have a look at the GitHub repo
In order to use the HAL MessageConverter you should have a Resource as a paramter. Try changing your code to:
public PersistentEntityResource postShoppingList(#RequestBody Resource<ShoppingList> shoppingList)
In my case the problem was a difference between the field name at the pojo and the json field. i.e:
#Entity
public class Pojo{
#ManyToOne(fetch = FetchType.EAGER, cascade = { CascadeType.ALL })
#JoinColumn(name = "fk_myother")
public Other myOther;
...etc
}
POST -> /dao/pojos
{
"myother":"http://localhost:8034/dao/others/50"
}
pay attention that the json field should be "myOther" instead.
Hope this helps to someone.

Categories