Spring-AMQP - routing based on message headers - spring-boot

As per the documentation: https://docs.spring.io/spring-amqp/docs/2.2.5.RELEASE/reference/html/#async-annotation-driven
We can have different handlers for messages based on it's converted class type like:
#RabbitListener(id="multi", queues = "someQueue")
#SendTo("my.reply.queue")
public class MultiListenerBean {
#RabbitHandler
public String thing2(Thing2 thing2) {
...
}
#RabbitHandler
public String cat(Cat cat) {
...
}
#RabbitHandler
public String hat(#Header("amqp_receivedRoutingKey") String rk, #Payload Hat hat) {
...
}
#RabbitHandler(isDefault = true)
public String defaultMethod(Object object) {
...
}
}
This I believe won't be great in performance since it has to do a trial and error to cast the incoming payload.
Instead, how to filter based on a condition say a header value? If header['operation']="order" then cast the message payload to Order class.

This I believe won't be great in performance since it has to do a trial and error to cast the incoming payload.
Usually, type information is conveyed in headers and the MessageConverter uses that information to create the payload - there is no "trial and error".
If you don't use one of the supplied converters, you can create your own, based on header['operation'].

Related

How to Handle FORM_URL_ENCODED to POJO with Quarkus?

I can't seem to find any information on how to have Quarkus convert incoming URL_FORM_ENCODED requests into a POJO.
The documentation says I can annotate each parameter in my receiving method with the #RestForm parameter. But, in the case where we have a lot of form params that would mean I have to add each parameter to my method signature.
I tried simply doing something like:
public class FormStuff {
#RestForm
public String title;
....More fields
}
and then in my receiving method I have:
#POST
#Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public void save(FormStuff stuff) {
log.info("Got Stuff! {}", stuff);
}
However, when I call this from my webpage I always get a 415 Unsupported MediaType
BUT! If I change my signature to:
#POST
#Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public void save(#RestForm title) {
log.info("Got Stuff! {}", title);
}
The request goes through and I see the title field print out.
Does Quarkus support converting Forms to POJO like it does for MultiPart forms?
Thanks!
You need to do the following:
#POST
#Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public void save(#BeanParam FormStuff stuff) {
log.info("Got Stuff! {}", stuff);
}
#BeanParam is a JAX-RS standard annotation that users can use to gather multiple HTTP params into a POJO.

How to validate request parameters on feign client

Is there a way to add validation to feign clients on the request parameters.
For example:
#FeignClient
public interface ZipCodeClient {
#GetMapping("/zipcodes/{zipCode}")
Optional<ZipCodeView> findByZipCode(#PathVariable("zipCode") String zipCode);
}
It would be nice to verify that zipcode is not empty and is of certain length etc, before sending the HTTP call to the server.
If your validations are simple, apply to only headers and query string parameters, you can use a RequestInterceptor for this, as it provides you the opportunity to review the RequestTemplate before it is sent to the Client.
public class ValidatingRequestInterceptor implements RequestInterceptor {
public void apply(RequestTemplate requestTemplate) {
// use the methods on the request template to check the query and values.
// throw an exception if the request is not valid.
}
}
If you need to validate the request body, you can use a custom Encoder
public class ValidatingEncoder implements Encoder {
public void encode(Object object, Type type, RequestTemplate template) {
// validate the object
// throw an exception if the request is not valid.
}
}
Lastly, if you want to validate individual parameters, you can provide a custom Expander for the parameter and validate it there. You can look at this answer for a complete explanation on how to create a custom expander that can work with Spring Cloud.
How to custom #FeignClient Expander to convert param?
For completeness, I've included an example for how to do this with vanilla Feign.
public class ZipCodeExpander implements Expander {
public String expand(Object value) {
// validate the object
// throw an exception if the request is not valid.
}
}
public interface ZipCodeClient {
#RequestLine("GET /zipcodes/{zipCode}")
Optional<ZipCodeView> findByZipCode(#Param(expander = ZipCodeExpander.class) ("zipCode") String zipCode);
}
As pointed out in this comment, a solution using the Bean Validation API would be nice. And indeed, I found in a Spring Boot project that merely placing #org.springframework.validation.annotation.Validated on the interface is sufficient for enabling Bean Validation.
So for example:
#FeignClient
#Validated
public interface ZipCodeClient {
#GetMapping("/zipcodes/{zipCode}")
Optional<ZipCodeView> findByZipCode(#PathVariable("zipCode") #NotEmpty String zipCode);
}
triggering a ConstraintViolationException in the case of violations.
Any standard Bean Validation feature should work here.
UDPATE Note that there seems to be a potential issue with this solution that might require setting a Hibernate Validator configuration property like this: hibernate.validator.allow_parallel_method_parameter_constraint=true

Customization of Spring ConversionFailedException error HTTP status

I have an enum class:
class enum Type {
LOCAL, REMOTE
}
I have an API that accepts the enum as a GET parameter
#RequestMapping(method = RequestMethod.GET, location="item", params = "type")
public Item[] get(Type type) {
...
When a client calls the API with valid values, like GET /item?type=LOCAL or GET /item?type=REMOTE it works fine. If the client supplies invalid value for type, e.g. GET /item?type=INVALID_TYPE, then Spring generates 500 Internal Server Error. I would like to turn it into 400 Bad Request validation error, potentially adding useful information for the client. I prefer to reuse the built type converter since in works just fine, just want to change a type of error HTTP thrown with minimum changes.
I believe if you add the right exception to #ControllerAdvice, you can customize the response. In this case, I found that MethodArgumentTypeMismatchException was the one in question.
#ExceptionHandler(MethodArgumentTypeMismatchException.class)
public void methodArgumentTypeMismatchException(final HttpServletResponse response) throws IOException {
response.sendError(BAD_REQUEST.value());
}
Why is this happening?
I would consider having a look at the example here about the #ControllerAdvice and/or #ExceptionHandler annotations. The error you're experiencing is occurring because, I believe, Spring tries to construct a Type from the "INVALID_TYPE" string and gets an error when it cannot create a Type from it--because "INVALID_TYPE" is not one of the available values.
What can I do about it?
What you'll want to do is add a string constructor to your enum so it knows, more correctly how to create one of the enum objects, and then check the input to see if its valid. If it is invalid, throw a custom exception. Then, in your #ControllerAdvice, you can customize the HTTP status code of the response.
The exception will then be able to be handled with something like the following:
#ControllerAdvice
class GlobalControllerExceptionHandler {
#ResponseStatus(HttpStatus.BAD_REQUEST) // 409
#ExceptionHandler(MyCustomException.class)
public void handleConflict() {
// handle the exception response, if you need information about the
// request, it should be able to be attached to the custom exception
}
}
The enum would look something like this:
public enum Type{
LOCAL("LOCAL"),
REMOTE("REMOTE");
private String type;
private Type(String type) {
if(type.equals("LOCAL") || type.equals("REMOTE")) {
this.type = type;
} else {
throw new MyCustomException();
}
}
public String getType() {
return url;
}
}

Does retrofit interfaces support templated callbacks

I have an API the returns a standard reply for all requests that gets parsed by gson/retrofit.
public class ServerReply<T> {
#Expose
private String status;
#Expose
private T data;
#Expose
private String message;
}
I have an interface for Retrofit that will return a list of users inside of serverReply.
public interface Test {
#POST("/Test")
void runTest(#Body Body body, Callback<ServerReply<List<User>>> response);
}
I would like to get a different list of objects depending on the content of the body. Is it possible to use templating/generics to accomplish this?(see below)
public interface Test<T> {
#POST("/Test")
void runTest(#Body Body body, Callback<ServerReply<List<T>>> response);
}
No, but it's a Java limitation not a missing Retrofit feature. Due to type erasure there is no way for Retrofit to resolve what the type variable T actually is to pass to the deserializer without a concrete class.

How do you handle deserializing empty string into an Enum?

I am trying to submit a form from Ext JS 4 to a Spring 3 Controller using JSON. I am using Jackson 1.9.8 for the serialization/deserialization using Spring's built-in Jackson JSON support.
I have a status field that is initially null in the Domain object for a new record. When the form is submitted it generates the following json (scaled down to a few fields)
{"id":0,"name":"someName","status":""}
After submitted the following is seen in the server log
"nested exception is org.codehaus.jackson.map.JsonMappingException: Can not construct instance of com.blah.domain.StatusEnum from String value '': value not one of the declared Enum instance names"
So it appears that Jackson is expecting a valid Enum value or no value at all including an empty string. How do I fix this whether it is in Ext JS, Jackson or Spring?
I tried to create my own ObjectMapper such as
public class MyObjectMapper extends Object Mapper {
public MyObjectMapper() {
configure(DeserializationConfig.Feature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
}
}
and send this as a property to MappingJacksonMappingView but this didn't work. I also tried sending it in to MappingJacksonHttpMessageConverter but that didn't work. Side question: Which one should I be sending in my own ObjectMapper?
Suggestions?
The other thing you could do is create a specialized deserializer (extends org.codehaus.jackson.map.JsonDeserializer) for your particular enum, that has default values for things that don't match. What I've done is to create an abstract deserializer for enums that takes the class it deserializes, and it speeds this process along when I run into the issue.
public abstract class EnumDeserializer<T extends Enum<T>> extends JsonDeserializer<T> {
private Class<T> enumClass;
public EnumDeserializer(final Class<T> iEnumClass) {
super();
enumClass = iEnumClass;
}
#Override
public T deserialize(final JsonParser jp,
final DeserializationContext ctxt) throws IOException, JsonProcessingException {
final String value = jp.getText();
for (final T enumValue : enumClass.getEnumConstants()) {
if (enumValue.name().equals(value)) {
return enumValue;
}
}
return null;
}
}
That's the generic class, basically just takes an enum class, iterates over the values of the enum and checks the next token to match any name. If they do it returns it otherwise return null;
Then If you have an enum MyEnum you'd make a subclass of EnumDeserializer like this:
public class MyEnumDeserializer extends EnumDeserializer<MyEnum> {
public MyEnumDeserializer() {
super(MyEnum.class);
}
}
Then wherever you declare MyEnum:
#JsonDeserialize(using = MyEnumDeserializer.class)
public enum MyEnum {
...
}
I'm not familiar with Spring, but just in case, it may be easier to handle that on the client side:
Ext.define('My.form.Field', {
extend: 'Ext.form.field.Text',
getSubmitValue: function() {
var me = this,
value;
value = me.getRawValue();
if ( value === '' ) {
return ...;
}
}
});
You can also disallow submitting empty fields by setting their allowBlank property to false.
Ended up adding defaults in the EXT JS Model so there is always a value. Was hoping that I didn't have to this but it's not that big of a deal.
I have the same issue. I am reading a JSON stream with some empty strings. I am not in control of the JSON stream, because it is from a foreign service. And I am always getting the same error message. I tried this here:
mapper.getDeserializationConfig().with(DeserializationConfig.Feature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT);
But without any effect. Looks like a Bug.

Resources