Open murphye opened 4 years ago
Sounds good. Plz consider security constraints for Qute-Form (for example, JavaServerFaces has built in support for CSRF, etc.).
Does not offer companion form validation API
As we can already bind hibernate validation api to model or method, we can respond with validation errors by http response to the client.
Uses Jackson's ObjectMapper to convert the Map to JSON string.
Json-B?
@nimo23 What you speak of is interesting (security, validation), but is out of scope for this issue. This issue is only for the Form -> Object handling.
Side note: I have verified that @Valid works, but isn't very useful for form handling. I have ended up using the Validator API programmatically in my demo (work in progress) as shown here: https://github.com/murphye/spring-petclinic/blob/spring-boot-1.5-to-quarkus/src/main/java/org/springframework/samples/petclinic/owner/OwnerController.java#L158
What is offered by Spring is an abstraction on how the validation errors are captured with BindingResult
. I have no interest in Quarkus replicating this functionality.
@nimo23 I am not sure about JSON-B. I will say that Jackson is very intelligent on how it serializes and deserializes that make the sample implementation very straightforward. I do not know if JSON-B will work the same way.
@mkouba I would be interested to get your take on this proposal because it primarily targets users of Qute.
I really +1 this idea. I tried to implement a similar solution here
I reach a point where I think that we could really look at what Play2 did with the Java Form and a FormFactory. See the documentation, with Bean Validation and JSR-380. The idea is to define a Form<Owner>
and not to unmarshall the entity Owner
in your controller. This is then easier to return badRequest, or to re-display a template with errors (as we both did actually).
Let's keep posted and see if this could generate some ideas for Qute.
Description Developers are moving to a REST-centric world, along with SPA (i.e. Angular). It is very common to use JavaScript to generate ......
Very great solution, I've inspired on your code and made a fork which only is triggered when the request method is annotated with FormDataJson.java
, hope it helps.
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Methods using that annotation will have form data parsed to VO classes using Jackson Json
* deserializer
*/
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface FormDataJson {
}
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import javax.ws.rs.Consumes;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.ext.MessageBodyReader;
import javax.ws.rs.ext.Provider;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.jboss.resteasy.spi.util.FindAnnotation;
import io.netty.handler.codec.http.QueryStringDecoder;
import io.vertx.core.http.HttpServerRequest;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Provider
@Consumes({MediaType.APPLICATION_FORM_URLENCODED})
public class FormDataToJsonAdapterBodyReader implements MessageBodyReader<Object> {
private final ObjectMapper objectMapper = new ObjectMapper()
.enable(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS);
@Override
public boolean isReadable(Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType) {
return FindAnnotation.findAnnotation(annotations, FormDataJson.class) != null
|| type.isAnnotationPresent(FormDataJson.class);
}
@Override
public Object readFrom(Class<Object> type, Type genericType, Annotation[] annotations,
MediaType mediaType, MultivaluedMap<String, String> httpHeaders, InputStream entityStream)
throws WebApplicationException {
final var entity = convertStreamToString(entityStream);
final var params = new QueryStringDecoder(entity, false).parameters();
try {
final var multiValueJsonBytes = this.objectMapper.writeValueAsBytes(params);
return this.objectMapper.readValue(multiValueJsonBytes, type);
} catch (IOException e) {
log.warn("status=Error converting POST form body to JSON object.", e);
throw new WebApplicationException(Response
.status(Response.Status.BAD_REQUEST)
.entity(e.getMessage())
.build());
}
}
private static String convertStreamToString(java.io.InputStream is) {
java.util.Scanner s = new java.util.Scanner(is).useDelimiter("\\A");
return s.hasNext() ? s.next() : "";
}
}
Very interesting indeed.
@FroMage what's your take on this? Should we add something like this to RESTEasy Reactive?
I'd also like to hear @edeandrea's take on it.
Personally the idea of being able to bind a form with fields similar to a json structure is beneficial. I've always liked that Spring had that ability and that I did not have to create some brand new object just because I wanted to form post similar/same data.
That being said there are some things to keep in mind that I think are needed in order to make it a proper solution.
Someone mentioned csrf above. To me that is required - otherwise people could get themselves into trouble.
What about multi-form posting? In the Spring example I could use @ModelAttribute
across multiple POSTs and collect data across a set of forms, then "commit" the form on the last POST. Spring uses @SessionAttributes
for this, which would open up a whole other can of worms - how to persist the state across multiple http requests. I think Spring supports both session-based and flash-based. I remember seeing something re: flash attributes in Quarkus in @FroMage 's Renard presentation a few weeks ago.
Adding onto the multi-view form posting - you would definitely need a way to handle input validation at each "step", vs an all at once. You might be able to do that with validation groups, but I'm not as familiar with doing it in jax-rs. All my experience is with Spring (and I can certainly share how we did it of needed).
Speaking of Renard - seems to me this all might be a good fit for that?
Description Developers are moving to a REST-centric world, along with SPA (i.e. Angular). It is very common to use JavaScript to generate JSON from a
<form>
and submit it via AJAX. However, this is very complicated and error prone.With the addition of Qute, Quarkus would benefit from simplified
<form>
submission mechanism that is reliant on JSON rather than@Form
in JAX-RS or@ModelAttribute
in Spring (which is not even supported in Quarkus anyways). The mechanism would be completely server-side and not dependent on JavaScript.Benefits are:
<form>
s and JSON data for POJOs@ModelAttribute
in Spring<form>
use casesPotential drawbacks are:
BindingResult
)Implementation ideas The functionality I am describing does not require an API change. It just needs a
ContainerRequestFilter
or similar mechanism to be included with Quarkus. I have created a proof of concept here: FormDataToJsonFilter.javaIt works by:
APPLICATION_FORM_URLENCODED_TYPE
andAPPLICATION_JSON_TYPE
. This is the key for enabling this feature.POST
request and has a content type ofAPPLICATION_FORM_URLENCODED_TYPE
getEntityStream
to aString
. This contains the body of the POST request which are URL parameters in query string format.QueryStringDecoder
to convert to aMap
.ObjectMapper
to convert theMap
to JSON string.setEntityStream
"Content-Type"
header to beMediaType.APPLICATION_JSON
Sample implementation:
Here is how to use this functionality in Spring Web (notice there are 2 consumes):
Here is how to use it in JAX-RS (notice there are 2 consumes):
Existing Solutions Just for reference, here are the roughly equivalent usages with Spring and JAX-RS (there is usually more involved than this):
Spring using
@ModelAttribute
(doesn't work in Quarkus):JAX-RS using
@Form
(also requires using@FormItem
on the model object):Conclusion
I feel this should be included in Quarkus (rather than Resteasy) because it's an opinionated approach with a relatively simple solution as a standard filter (could be disabled through config too). This could be offered as the default mechanism for processing form data with Qute and Quarkus.
I hope you will consider adding it in the near future as it would ease some current pains with adopting the Spring APIs for form processing with Qute.
Further Notes
<form><select multiple>
items to convert to aList<String>
.