Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[418] multipart/form-data support #948

Merged
merged 10 commits into from
May 18, 2021

Conversation

andymc12
Copy link
Contributor

@andymc12 andymc12 commented Mar 4, 2021

Fixes #418

Includes new API, spec text and some simple examples.

@mkarg
Copy link
Contributor

mkarg commented Mar 5, 2021

@andymc12 Thank you for contributing this! Unfortunately I am busy with JavaLand organization recently, so the next two weeks I won't have time to actually review this code. Sorry for that. I pretend to have a look at it after JavaLand! :-)

@andymc12
Copy link
Contributor Author

@mkarg - I hope JavaLand went well - it looks like it was well attended with lots of notable speakers! Hopefully you have recovered and can review this PR?

@chkal @spericas - could you take a look as well? Thanks!

@mkarg
Copy link
Contributor

mkarg commented Mar 19, 2021

@mkarg - I hope JavaLand went well - it looks like it was well attended with lots of notable speakers! Hopefully you have recovered and can review this PR?

Thanks, Andy, yes, we had approx. 1500 online attendees and a quite shiny lineup of notable speakers. Despite all hopes, I am still busy today and tomorrow, so if things run well, will look into this PR on Sunday. Stay tuned! :-)

Copy link
Contributor

@mkarg mkarg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm done with an initial preview. Kudos, this is great work! But I have some questions, see inlined. Also I wonder why we force the use of streams? I understand that streams have benefits, by why not also allows parameter conversion or using Entity classes?

@andymc12
Copy link
Contributor Author

andymc12 commented Apr 1, 2021

Hi @mkarg @spericas - thanks for all of the feedback - excellent suggestions! I think I have addressed all of your concerns. Would you mind taking another look and approving if you think it's ready to go in? Thanks again.

@mkarg
Copy link
Contributor

mkarg commented Apr 1, 2021

Hi @mkarg @spericas - thanks for all of the feedback - excellent suggestions! I think I have addressed all of your concerns. Would you mind taking another look and approving if you think it's ready to go in? Thanks again.

@andymc12 Excellent PR, but please let's have Easter first. :-)

Copy link
Contributor

@chkal chkal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@andymc12 Thank you so much for working on this. See my comment in the source.

jaxrs-api/src/main/java/jakarta/ws/rs/ext/Part.java Outdated Show resolved Hide resolved
@andymc12
Copy link
Contributor Author

andymc12 commented Apr 8, 2021

@spericas @mkarg @chkal Thanks for the great review comments so far! I think I've addressed all concerns and suggestions. Would you mind taking another look and approving? Thanks!

spericas
spericas previously approved these changes Apr 9, 2021
Copy link
Contributor

@spericas spericas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Copy link
Contributor

@chkal chkal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@andymc12 Thanks a lot for integrating the latest changes. This looks awesome. I'm basically +1 for this, but added one comment. It would be great to hear your thoughts.

@andymc12
Copy link
Contributor Author

Added support for @FormParam("partName") SomeObject param in the spec text.

@chkal, @spericas, @mkarg, others - would you take another look and (re-)approve? Thanks!

spericas
spericas previously approved these changes Apr 13, 2021
annotation corresponds to the name of the part. The parameter type may be
a `jakarta.ws.rs.ext.Part`, a `java.io.InputStream`, a `String`, or any type
that can be converted to using a registered `MessageBodyReader` or
`ParamConverter`. Here is
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry. One last question.

The API docs of Part#getContent(Class|GenericType) only mentions MessageBodyReader, but here we allow ParamConverter as well.

I'm wondering if ParamConverter makes sense here. If we allow both, we will most likely have to define which one of the two mechanisms is preferred over the other. And ParamConverter uses a String as the input and in the next paragraph we explicitly warn about String being not optimal as it requires to read the full entity into the heap.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My thinking here was that ParamConverter providers are already responsible for converting Strings to parameter types. The only real difference here (with @FormParam) is that we have an InputStream instead of a a String - but since we allow the implementation to convert the InputStream to a String for @FormParam(...) String, it seemed intuitive (to me, at least :) ) that the implementation should first check that it can convert to the parameter type using registered MBRs, and if it finds none, then it could check to see if a ParamConverter is registered for the parameter type. If so, then the implementation would convert the part's InputStream to String and then invoke the ParamConverter.

For Part#getContent(...), I don't think it makes sense to use ParamConverter because at this point, it's not a parameter; the parameter was Part. A MBR does make sense because it is reading the message body of the individual part to the specified class or GenericType.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW, I appreciate the thoroughness of your review. I'd certainly rather spend time up front to get it right rather than trying to patch something that was broken from the start. :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@andymc12 Thanks for your explanation and sorry for the delayed reply.

After thinking about this more, it somehow makes sense to allow ParamConverter in case of @FormParam, as it is also used for standard @FormParam cases. However, I also think it could be a bit weird for the user that @FormParam and getContent may behave differently.

So basically we have three options:

  1. @FormParam uses the MBR and falls back to a ParamConverter (if available), but getContent() only uses the MBR.
  2. Both @FormParam and getContent() use the MBR and fall back to ParamConverter.
  3. Both @FormParam and getContent() only use the MBR.

To be honest, I'm not sure yet which option I personally like most.

@eclipse-ee4j/ee4j-jaxrs-committers Other opinions?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documentation of ParamConverter says it applies to parameters injected via (among other annotations) @FormParam.:

Defines a contract for a delegate responsible for converting between a String form of a message parameter value and the corresponding custom Java type T. Conversion of message parameter values injected via @PathParam, @QueryParam, @MatrixParam, @FormParam, @CookieParam and @HeaderParam is supported.

I am not aware of an existing API method that would utilize converters. Therefore to maintain consistency, I'd suggest staying with option 1.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. @FormParam uses the MBR and falls back to a ParamConverter (if available), but getContent() only uses the MBR.
  2. Both @FormParam and getContent() use the MBR and fall back to ParamConverter.
  3. Both @FormParam and getContent() only use the MBR.

getContent() is not intended for parameters, so it shouldn't use ParamConverter.

My preference would be 1.

Actually after some thinking I wonder why we want @FormParam to use MBR, actually, as it is a parameter.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my view @FormParam on types other than EntityPart is basically a convenient way to parse and access part data.

So instead of doing this:

public Response someMethod( List<EntityPart> parts ) {

  JsonObject data = parts.stream()
    .filter( p -> p.getId().equals("file") )
    .findFirst()
    .map( p -> p.getContent( JsonObject.class ) )
    .orElse( null );

   // ...

}

or instead of this:

public Response someMethod( @FormParam("file") EntityPart part ) {

  JsonObject data = part.getContent( JsonObject.class );
  // ...

}

You can simply do this:

public Response someMethod( @FormParam("file") JsonObject data ) {
  // ...
}

And therefore it would make sense that using @FormParam on some specific type T is the same as calling EntityPart.getContent(T.class).

However, I agree that this may be problematic given the originally intended usage of ParamConverter and MessageBodyReader.

@jansupol
Copy link
Contributor

@andymc12 Part seems to be quite a general name. Possibly, a part of anything else can arise and the name would clash. Would it be better to tell Part of what this class is? EntityPart perhaps?

@mkarg
Copy link
Contributor

mkarg commented Apr 15, 2021

Speaking in terms of Java, it would be Entity.Part, as it is not to be seen as an alternative to an entity, but as a part of it.

@andymc12
Copy link
Contributor Author

@jansupol @mkarg

@andymc12 Part seems to be quite a general name. Possibly, a part of anything else can arise and the name would clash. Would it be better to tell Part of what this class is? EntityPart perhaps?

Speaking in terms of Java, it would be Entity.Part, as it is not to be seen as an alternative to an entity, but as a part of it.

I see that Part is used in the servlet spec (for pretty much the same thing as we're using it for here) and in the Java/Jakarta Mail spec. I think it's probably rare enough of a usage that it probably won't clash (and worst case, the package name isn't super long), but I'm not averse to renaming it.

I like Markus's idea of Entity.Part, but the Entity class is in the client package. Since this class can be used on both the client and server, I'd like to avoid confusion and would probably go with EntityPart in the ext package.

@andymc12
Copy link
Contributor Author

Renamed Part to EntityPart.

@spericas @chkal @mkarg @jansupol could you take another look and approve? Thanks again!

@mkarg
Copy link
Contributor

mkarg commented Apr 22, 2021

@jansupol @mkarg

@andymc12 Part seems to be quite a general name. Possibly, a part of anything else can arise and the name would clash. Would it be better to tell Part of what this class is? EntityPart perhaps?

Speaking in terms of Java, it would be Entity.Part, as it is not to be seen as an alternative to an entity, but as a part of it.

I see that Part is used in the servlet spec (for pretty much the same thing as we're using it for here) and in the Java/Jakarta Mail spec. I think it's probably rare enough of a usage that it probably won't clash (and worst case, the package name isn't super long), but I'm not averse to renaming it.

I like Markus's idea of Entity.Part, but the Entity class is in the client package. Since this class can be used on both the client and server, I'd like to avoid confusion and would probably go with EntityPart in the ext package.

Another idea would be to actually use Entity.Part, but to refactor it in a future release so it is getting deprecated in favor of the same class found in the core package. That would be straightforward.

@andymc12
Copy link
Contributor Author

andymc12 commented May 4, 2021

@chkal @spericas

@andymc12 The latest changes look good. I think at some point we need to answer @chkal 's question about ParamConverter's but, honestly, I'm not sure which option is best at this time. Perhaps we need more time using the API to determine that.

Agreed on this one. Maybe we should only allow @FormParam for EntityPart for now? We can still add support for other types later on if we want.

What if we also left in the ability to automatically convert to InputStream and String? i.e.

public Response someMethod( @FormParam("file") InputStream data ) {
  // ...
}

and

public Response someMethod( @FormParam("file") String data ) {
  // ...
}

This should avoid the confusion with ParamConverters, but still allow some of the more common use cases - like writing a part to disk or reading it directly as a String.

This would mean that code block 1 and 2 from my comment would work fine. Just code block 3 won't be possible. But IMO that would be acceptable.

That should be fine. The only difference between your 2nd block and 3rd is one line of code. I can live with that! :-)

@spericas
Copy link
Contributor

spericas commented May 5, 2021

@chkal @spericas

@andymc12 The latest changes look good. I think at some point we need to answer @chkal 's question about ParamConverter's but, honestly, I'm not sure which option is best at this time. Perhaps we need more time using the API to determine that.

Agreed on this one. Maybe we should only allow @FormParam for EntityPart for now? We can still add support for other types later on if we want.

What if we also left in the ability to automatically convert to InputStream and String? i.e.

public Response someMethod( @FormParam("file") InputStream data ) {
  // ...
}

and

public Response someMethod( @FormParam("file") String data ) {
  // ...
}

This should avoid the confusion with ParamConverters, but still allow some of the more common use cases - like writing a part to disk or reading it directly as a String.

This would mean that code block 1 and 2 from my comment would work fine. Just code block 3 won't be possible. But IMO that would be acceptable.

That should be fine. The only difference between your 2nd block and 3rd is one line of code. I can live with that! :-)

This seems like a reasonable solution at this time; getting the param converters in the mix seems reasonable but also adds additional complexity.

andymc12 added 10 commits May 5, 2021 08:44
Signed-off-by: Andy McCright <j.andrew.mccright@gmail.com>
- "ex:" to "e.g."
- Using modern java.nio.file examples instead of java.io

Signed-off-by: Andy McCright <j.andrew.mccright@gmail.com>
- Change "entityStream" to "content"
- Indicate who must close content streams
- Update examples to use new method name / close behavior

Signed-off-by: Andy McCright <j.andrew.mccright@gmail.com>
Also, add `withFileName` convenience method and document the encoding
charset to use when converting a part's content.

Signed-off-by: Andy McCright <j.andrew.mccright@gmail.com>
Signed-off-by: Andy McCright <j.andrew.mccright@gmail.com>
Signed-off-by: Andy McCright <j.andrew.mccright@gmail.com>
Signed-off-by: Andy McCright <j.andrew.mccright@gmail.com>
- Moves `EntityPart` from `ext` to `core` package
- Adds new `content` methods to `EntityPart.Builder` that use MBWs
- Cleanup of references to old `Part` name updated to `EntityPart`

Signed-off-by: Andy McCright <j.andrew.mccright@gmail.com>
Signed-off-by: Andy McCright <j.andrew.mccright@gmail.com>
Signed-off-by: Andy McCright <j.andrew.mccright@gmail.com>
@andymc12 andymc12 dismissed stale reviews from spericas and chkal via b5319b0 May 5, 2021 14:44
@andymc12 andymc12 force-pushed the 418-multipart-support branch from eb54a83 to b5319b0 Compare May 5, 2021 14:44
@andymc12
Copy link
Contributor Author

andymc12 commented May 5, 2021

The latest commit should address @chkal's question - basically allowing code blocks 1 & 2, but not 3. It also allows String and InputStream as @FormParam types. I believe that this was the last concern.

@spericas @chkal @mkarg @pdudits @jansupol @deki - thanks for all of the discussion and reviews! Can I ask that you take another look and approve?

Copy link
Contributor

@chkal chkal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good!

@andymc12 Thanks a lot for all the adjustments. :-)

@andymc12
Copy link
Contributor Author

@spericas @mkarg @pdudits @jansupol @deki - I think we're all happy with these changes (if not, please let me know). If one of you could approve this PR, I'll go ahead and merge. Thanks!

@spericas spericas self-requested a review May 18, 2021 13:14
@andymc12 andymc12 merged commit 48987b9 into jakartaee:master May 18, 2021
@kalgon
Copy link

kalgon commented May 19, 2021

Hi, I have a simple remark: can't EntityPart use javax.ws.rs.core.HttpHeaders ?

@andymc12
Copy link
Contributor Author

Hi, I have a simple remark: can't EntityPart use javax.ws.rs.core.HttpHeaders ?

Not directly, but you can specify headers when building an EntityPart or read them when consuming one - e.g.:

EntityPart spanishPart = EntityPart.withName("spanishData").header(HttpHeaders.CONTENT_LANGUAGE, "es")...

or

String language = entityPart.getHeader(HttpHeaders.CONTENT_LANGUAGE); 
assertEquals("es", language);

@mkarg
Copy link
Contributor

mkarg commented May 22, 2021

@andymc12 Great feature! Thank you for driving this!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support for MultiPart forms
8 participants