Manipulate InputStream of ContainerRequestFilter - java

I have a encrypted string being sent by a client. I am trying to intercept the string using ContainerRequestFilter then decrypt it and set the InputStream again so that it can be used by Jackson to map to a POJO.
Illustration:
My Resource
#Path("auth")
public class AuthResource {
#POST
public Response testResource(#Auth AuthUser auth, Person person) {
System.out.println("Recieved Resource:: "+ new Gson().toJson(person));
return null;
}
}
Person.java
public class Person {
private String name;
private int age;
public Person() {};
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
My Filter
#Provider
public class MyFilter implements ContainerRequestFilter {
#Override
public void filter(ContainerRequestContext requestContext) throws IOException {
InputStream inputStream = requestContext.getEntityStream();
StringWriter writer = new StringWriter();
IOUtils.copy(inputStream, writer, "UTF-8");
String theString = writer.toString();
String decryptedMessage = "";
try {
decryptedMessage = JwtToken.decryptPayload(theString);
System.err.println("Decrypted Message: "+decryptedMessage);
} catch (Exception e) {
e.printStackTrace();
}
InputStream stream = new ByteArrayInputStream(decryptedMessage.getBytes(StandardCharsets.UTF_8));
requestContext.setEntityStream(stream);
}
}
I understand that once the InputStream is utilized it cannot be used again. But using requestContext.setEntityStream(stream); I am trying to set the InputStream again to be utilized by Jackson.
Inspite of that I am still unable to get the person object in my resource. The decryption is working fine as I have tested it using a debugger.
I get the following error: 415: Unsupported Media Type
Edit 1: I am using Adavanced Rest Client to hit the url
http://localhost:8080/auth
Header:
authorization: Basic ZXlKaGRYUm9iM0pwZW1GMGFXOXVJam9pWVcxcGRDSXNJbUZzWnlJNklraFRNalUySW4wLmUzMC5MLUtmOUxQNjFSQ21Bektob2lTR0V4bEJ3RXRUMXhrR3A3bUpIZmFSeV9FOnBhc3M=
Raw Payload:
eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiQW1pdCIsImFnZSI6MjJ9.-rO6yhYJ--3ZzVCaHFw1hF-s533foYY6vVAuyRh3Q9g
The payload is encrypted simply using JWT:
Jwts.builder().setPayload(new Gson().toJson(new Person("Amit",22))).signWith(SignatureAlgorithm.HS256, key).compact();

Your request payload is not a JSON. It's a JWT token which contains a JSON encoded as Base64. It's a piece of text. Hence, the Content-Type of the request should be text/plain instead of application/json:
POST /api/auth HTTP/1.1
Host: example.org
Content-Type: text/plain
eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiQW1pdCIsImFnZSI6MjJ9.-rO6yhYJ--3ZzVCaHFw1hF-s533foYY6vVAuyRh3Q9g
Your filter modifies the payload of the request: the filter gets the JWT token from the request payload, gets the token payload, decodes the token payload into a JSON string and sets the JSON string to the request payload.
After the executing of the filter, the request will contain a JSON string and not just a piece of text. Hence, after that, the Content-Type of the request should be modified to application/json. It could be achieved with the following lines:
requestContext.getHeaders().remove(HttpHeaders.CONTENT_TYPE);
requestContext.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON);
To ensure the filter will be executed before the resource matching, annotate your filter with #PreMatching.
And don't forget to annotate your resource method with #Consumes(MediaType.APPLICATION_JSON).

I tried changing the payload to a JSON document as:
"temp":"eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiQW1pdCIsImFnZSI6MjJ9.-rO6yhYJ--3ZzVCaHFw1hF-s533foYY6vVAuyRh3Q9g"
and extracted the encrypted payload using the below code in my filter:
JsonObject jsonObject = new Gson().fromJson(theString, JsonObject.class);
theString = jsonObject.get("temp").getAsString();
Also changed the resource to #Consumes(MediaType.APPLICATION_JSON)
And it worked!
I guess the reason was as mentioned by pandaadb that Jersey was unable to recognize it as a JSON document and hence was unable to Map it to the Person POJO.

Related

HTTP POST Angular to Java. Cannot send multiple parameters with different types

I am trying to send an uploaded file (FormData from Angular) and a string in the same HTTP POST request to backend (Java using Grizzly server and Ajax for REST services).
The problem is that I receive HTTP 400 Bad Request because the file is not correctly mapped:
jersey message: Can not construct instance of java.io.InputStream: abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type
In Frontend I have a class named Form created using ng g class Form, which contains:
export class Form {
private file:FormData;
private bookName: String;
constructor(file:FormData, bookName: String) {
this.file = file;
this.bookName = bookName;
}
}
The HTTP POST method from Frontend is:
sendFormData() {
const form = new Form(this.testData, this.bookName);
this.pdfService.sendFormData(form).subscribe((res) => {
console.log(res);
});
}
Above this.testData has the type FormData and this.bookName is a string. They both contain the expected input values.
The pdfService.sendFormData is:
public sendFormData(form: Form) {
console.log("sending to " + this.baseUrl + "uploadFile")
return this.http.post(this.baseUrl + "uploadFile", form, { responseType: 'text' });
}
In Backend I have a class Form.java (the mapped class):
public class Form {
String bookName;
InputStream file;
... (getters & setters & constructor)
}
and the HTTP POST method is:
#POST
#Consumes(MediaType.APPLICATION_JSON)
#Produces(MediaType.TEXT_HTML)
#Path("uploadFile")
public Response convertPdfToHtml(Form form) {
...
}
To get the mapped string I use: form.getBookName() and to get the mapped file I use: form.getFile().
As I said, the problem is that the file from Frontend is not correctly mapped on the InputStream from Backend.
What type should I use to map a FormData from Frontend to a type in Backend? Or what other implementations could I use to send a file and a string in the same POST request?
Thanks.
in POST method you should provide object of type FormData, and there
export class Form {
private file: File;
private bookName: String;
constructor(file:File, bookName: String) {
this.file = file;
this.bookName = bookName;
}
}
public sendFormData(form: Form) {
const formData = new FormData();
formData.append('bookName', form.bookName);
formData.append('file', form.file, form.file.name);
console.log("sending to " + this.baseUrl + "uploadFile")
return this.http.post(this.baseUrl + "uploadFile", formData, {responseType: 'text'});
}
and you'll get post with content-type multipart/form-data

Error sending MultipartFile to REST API using Spring Boot and Open feign

I'm trying to attach a file to send to and endpoint as a MultipartFile but I'm getting this exception:
Expected no exception to be thrown, but got 'feign.codec.EncodeException'
//...
Caused by: feign.codec.EncodeException: Could not write request:
no suitable HttpMessageConverter found for request type [java.util.LinkedHashMap]
and content type [multipart/form-data]
My method is:
//...
final User user
//...
#Override
DocumentResponse attachDocument(File file, String userId, String documentId) {
String timestamp = String.valueOf(System.currentTimeMillis())
String url = "${myProperties.apiUrl}/documents/attach?ts=${timestamp}"
String digest = myJWT.sign(HttpMethod.POST, url)
MultipartFile multiFile = new MockMultipartFile("test.xml",
new FileInputStream(file))
DocumentResponse documentResponse = user.attachDocument(multiFile,
userId, documentId, timestamp, digest)
return documentResponse
}
My interface is:
#FeignClient(name = 'myUser', url = '${apiUrl}', configuration = myConfiguration)
interface User {
//...
#PostMapping(value = '/documents/attach', consumes = 'multipart/form-data')
DocumentResponse attachDocument(#PathVariable('file') MultipartFile multiFile,
#PathVariable('clientId') String userId,
#PathVariable('documentId') String documentId,
#RequestParam('ts') String timestamp,
#RequestParam('digest') String digest)
}
And my configuration file is:
#Slf4j
#Configuration
class myConfiguration {
#Bean
Retryer feignRetryer(#Value('${feign.client.config.myUser.period}') Long period,
#Value('${feign.client.config.myUser.maxInterval}') Long maxInterval,
#Value('${feign.client.config.myUser.maxAttempts}') Integer maxAttempts) {
return new Retryer.Default(period, maxInterval, maxAttempts)
}
#Bean
ErrorDecoder errorDecoder() {
return new ErrorDecoder() {
#Override
Exception decode(String methodKey, Response response) {
if (HttpStatus.OK.value() != response.status()) {
FeignException ex = FeignException.errorStatus(methodKey, response)
if (response.status() != HttpStatus.BAD_REQUEST.value()) {
return new RetryableException('getting conflict and retry', new Date(System.currentTimeMillis() + TimeUnit.SECONDS
.toMillis(1)))
}
return new MyDocumentException()
}
}
}
}
}
Also, I have tried to add this code to myConfiguration file:
#Bean
Encoder encoder() {
return new FormEncoder()
}
But I have another exception:
Cannot cast object 'feign.form.FormEncoder#5fa78e0a'
with class 'feign.form.FormEncoder' to class 'java.beans.Encoder'
I'm using Spring boot '2.0.2.RELEASE' with:
"io.github.openfeign.form:feign-form:3.4.1",
"io.github.openfeign.form:feign-form-spring:3.4.1",
I checked these posts:
How to send POST request by Spring cloud Feign
no suitable HttpMessageConverter found for response type
Could not write request: no suitable HttpMessageConverter found for request type and content type
Converting file to multipartfile
Any suggestion?
feign.codec.EncodeException raised when a problem occurs encoding a message.
I think the #PathVariable('file') MultipartFile multiFile, can be converted to a base64 sting and pass it to REST API or add an Encoder to MultipartFile

Simple restful JSON POST with java as server and jquery as client

Before I ask my question I have to say that I have read more than 20 questions and articles about this problem and none of them could solve it.
My problem is I have a restful server in java like this:
#RequestMapping (value = "/downloadByCode", method = RequestMethod.POST)
#ResponseBody
public void downloadByCode(#RequestBody final String stringRequest, final HttpServletResponse response)
{
try
{
final ObjectMapper objectMapper = new ObjectMapper();
final JsonNode jsonRequest = objectMapper.readValue(stringRequest, JsonNode.class);
// ...
// some processings here to create the result
// ....
final ServletOutputStream outputStream = response.getOutputStream();
outputStream.write(result);
// Flush the result
outputStream.flush();
}
catch (final Exception exception)
{
LOG.debug("Exception Thrown [downloadByCode]", exception);
}
}
And I have tried different ways to send a json to this server with jquery (but all of them create errors):
$.ajax({
url:"/downloadByCode",
type:"POST",
data: JSON.stringify({"name":"value"}) });
415 "errors message" : "Content type 'application/x-www-form
urlencoded;charset=UTF-8' not supported", "type" :
"HttpMediaTypeNotSupportedError"
So I tried to fix it by adding contentType:
$.ajax({
url:"/downloadByCode",
contentType:"application/json",
type:"POST",
data: JSON.stringify({"name":"value"}) });
400 "errors message" : "Could not instantiate JAXBContext for class
[class java.lang.String]: null; nested exception is
javax.xml.bind.JAXBException\n - with linked
exception:\n[java.lang.NullPointerException", "type"
:"HttpMessageConversionError"
I tried to send json object directly instead of JSON.stringify and it gives the same 400 error.
I tried to add different consumes to the #requestMapping but still no luck.
I tried to define my own class instead of JsonNode but it does not change anything.
Any ideas?
Please try to create new class :
public class InputData{
private String name;
public String getName(){
return name;
}
public void setName(String name){
this.name = name;
}
}
Then
public void downloadByCode(#RequestBody InputData stringRequest, final HttpServletResponse response)
And
$.ajax({
url:"/downloadByCode",
contentType:"application/json",
type:"POST",
data: JSON.stringify({"name":"value"}) });
try #RequestBody final Map<String, String> stringRequest
also you will need consumes = "application/json" on the #RequestMapping because you have that in your AJAX call
You will get 400 if spring doesn't like the format in which you send your ajax - I've had so much trouble with this in the past and it seems better to just ignore header types and content types unless necessary
You might try sending your response back as a ResponseEntity instead of using the HttpServletResponse directly. My hunch is that second argument, the HttpServletRequest argument, is what is causing the problem. I've never used that. I've always send my response back using the spring mvc api.
With Jersey api you can try just:
#POST
public void downloadByCode(String stringRequest)
and I think you'll find the body of your post in stringRequest.
You can take request body as string with usage of org.springframework.http.HttpEntity<String> as request type, here is example with your code as base:
#RequestMapping (value = "/downloadByCode", method = RequestMethod.POST)
#ResponseBody
public void downloadByCode(final HttpEntity<String> request, final HttpServletResponse response)
{
try
{
final ObjectMapper objectMapper = new ObjectMapper();
final JsonNode jsonRequest = objectMapper.readValue(request.getBody(), JsonNode.class);
// ...
// some processings here to create the result
// ....
final ServletOutputStream outputStream = response.getOutputStream();
outputStream.write(result);
// Flush the result
outputStream.flush();
}
catch (final Exception exception)
{
LOG.debug("Exception Thrown [downloadByCode]", exception);
}
}
But maybe it will be better to use also String as return type, if you are planning to return result as string value, like this:
#RequestMapping (value = "/downloadByCode", method = RequestMethod.POST)
#ResponseBody
public String downloadByCode(HttpEntity<String> request) {
String requestBody = request.getBody();
String result;
// ...
// some processings here to create the result text
// ....
return result;
}
I made simple application using Spring Boot with usage of proposed solutions using HttpEntity and also additional example of usage POJO, to run application you need to have Maven and JDK >= 1.7.
#clonning repository with sample
git clone git#github.com:mind-blowing/samples.git
#change current folder to sample application root
cd samples/spring-boot/rest-handler-for-plain-text
#running application using maven spring-boot plugin
mvn spring-boot:run
After application will be started you can open http://localhost:8080 and you will see html page with simple usage of JQuery to send POST requests, text of request and response will visible on html page, in controller I added two handlers, first with usage of HttpEntity and second with usage of POJO.
Controller: SampleRestController.java
HTML page: index.html
Project: https://github.com/mind-blowing/samples/tree/master/spring-boot/rest-handler-for-plain-text
First of all If you are using maven you should add dependency for jackson
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.4.1</version>
</dependency>
or you can download the jar and put it in our project class path (you can use other mapper as well)
then you should create a model or DTO class where you can map your json
public class Data{
private String name;
pubilc Data(){}
//getter and setter
}
THEN you controller
#RequestMapping (value = "/downloadByCode", method = RequestMethod.POST)
#ResponseBody
public Data downloadByCode(#RequestBody final Data data, final HttpServletResponse response)
{
//your code
return data;
}
AJAX CALL
$.ajax({
url:"/downloadByCode",
contentType:"application/json",
type:"POST",
data: JSON.stringify({"name":"value"}) });
(Optional)You can override behavior by telling object mapper not to fail on missing properties by defining the bean as follows:
#Bean
public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
converter.setObjectMapper(new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false));
return converter;
}
http://websystique.com/springmvc/spring-mvc-requestbody-responsebody-example/
Looking at your errors, it's clear that you have configured 'Jaxb2RootElementHttpMessageConverter' or similar XML converter in your spring configuration. And since you have registerned an XML converter, the #RequestBody and #ResponseBody work based on the registered message converters.
So, to solve your problem, go with a JSON message converter such as 'MappingJacksonHttpMessageConverter'. Once you register a JSON message converter, create a bean class to hold your json data and use it with RequestBody as below:
// It has to meet the json structure you are mapping it with
public class YourInputData {
//properties with getters and setters
}
Update 1:
Since you have defined multiple message converters, Spring tries to use the first one available by default. In order to use specific message converter(in this case Jackson converter), you should specify 'Accept' header from client like below:
$.ajax({
headers: {
"Accept" : "application/json; charset=utf-8",
"Content-Type": "application/json; charset=utf-8"
}
data: "data",
success : function(response) {
...
} })
The final answer is a combination of a number of answers/comments in this question that I am going to summarize them here:
1- You have to make sure you have an appropriate json converter in your spring config such as MappingJacksonHttpMessageConverter (credits to #java Anto)
2- You have to create a POJO class with same structure as your json object (see #Vinh Vo answer)
3- Your POJO class cannot be an inline class unless it is a static class. It means it should have its own java file or it should be static. (credits to #NTyler)
4- Your POJO class can miss parts of your json object if you set it appropriately in your object mapper (see #Aman Tuladhar answer)
5- Your ajax call requires contentType:"application/json", and you should send your data with JSON.stringify
Here is the Final code that is working perfectly:
public static class InputData
{
private String name
public String getName()
{
return name;
}
public void setName(final String name
{
this.name = name;
}
}
#RequestMapping(value = "/downloadByCode", method = RequestMethod.POST)
#ResponseBody
public void downloadByCode(#RequestBody final InputData request, final HttpServletResponse response)
{
try
{
String codes = request.getName();
// ...
// some processings here to create the result
// ....
final ServletOutputStream outputStream = response.getOutputStream();
outputStream.write(result);
// Flush the result
outputStream.flush();
}
catch (final Exception exception)
{
LOG.debug("Exception Thrown [downloadByCode]", exception);
}
}
And it is the jquery Ajax request:
$.ajax({
url:"/downloadByCode",
contentType:"application/json",
type:"POST",
data: JSON.stringify({"name":"value"}) });
Delete the #ResponseBody on your downloadByCode method
Change your method downloadByCode() return type to String and then return the String
Response body will automatically convert the returned String to JSON and then use the data appropriately
I am not that well versed with java but as much as I know your java code must be something like this.
public class downloadByCode{
#GET
#Produces(MediaType.APPLICATION_JSON + ";charset=utf-8")
public Response downloadByCode(#QueryParam("paramater1") final String parameter 1, #Context HttpServletRequest httpRequest) {
If this not helps you can keep you code somewhere and share it.

No form params in rest resource for form urlencoded when using Content, but application/json works?

I have a REST endpoint #POST where the form params are null when the Content-Type is application/x-www-form-urlencoded. There is a ContainerRequestFilter earlier in the chain (code at the bottom) that takes the request, changes the stream to a BufferedInputStream, and then logs the request. If I remove this logging code, the endpoint has the correct form params. Otherwise, they're null and I can't figure out why.
Now if I use application/json, my endpoint has the correct params regardless if the logger is enabled or disabled.
I need application/x-www-form-urlencoded because the REST endpoint needs to redirect and browsers prevent redirection if the request isn't standard (preflight)
REST Endpoint that isn't working (OAuthRequest has null members)
#Stateless
#Path("v1/oauth2")
#Consumes(MediaType.APPLICATION_FORM_URLENCODED)
#Produces(MediaType.APPLICATION_JSON)
public class OAuthTokenResource {
#POST
public Response getToken(#Form OAuthRequest oauthRequest) {
...
}
OAuthRequest
public class OAuthRequest {
#FormParam(OAuthParam.CLIENT_ID)
#JsonProperty(OAuthParam.CLIENT_ID)
private String clientId;
#URL
#FormParam(OAuthParam.REDIRECT_URI)
#JsonProperty(OAuthParam.REDIRECT_URI)
private String redirectUri;
#FormParam(OAuthParam.USERNAME)
private String username;
#FormParam(OAuthParam.PASSWORD)
private String password;
...
}
Logging Filter
#Override
public void filter(final ContainerRequestContext context) throws IOException {
...
if (logEntity && context.hasEntity()) {
context.setEntityStream(logInboundEntity(builder, context.getEntityStream(), context.getMediaType()));
}
logger.debug(builder.toString());
}
private InputStream logInboundEntity(final StringBuilder builder, InputStream stream, MediaType mediaType) throws IOException {
if (!stream.markSupported()) {
stream = new BufferedInputStream(stream);
}
stream.mark(maxEntitySize + 1);
final byte[] entity = new byte[maxEntitySize + 1];
final int entitySize = stream.read(entity);
if ( entitySize > 0 ) {
String body = new String(entity, 0, Math.min(entitySize, maxEntitySize), StandardCharsets.UTF_8);
builder.append("\nBody: ");
builder.append(body);
}
if (entitySize > maxEntitySize) {
builder.append(MORE_INDICATOR);
}
stream.reset();
return stream;
}
Okay I am still not sure why #Form and #FormParam does not work if the InputStream is read during the filter chain.
But, I discovered a workaround as follows.
#POST
public Response getToken(MultivaluedMap<String, String> formParams) {
...
}
This provides the same behavior as during application/json as the params are already set even if the InputStream has been consumed.
However ultimately we went with disabling logging of the request body in our filter for security reasons.

Java consume JSON list from Rest service GET

The error I receive:
SEVERE: A message body reader for Java class java.util.List,
and Java type java.util.List<com.testapp.Category>,
and MIME media type text/html; charset=utf-8 was not found
Trying to consume a JSON response from a Rest service using the GET method with Jersey. The response from the server looks like this when I use curl:
[{"category":{"id":"4d9c5dfc8ddfd90828000002","description":"Cows"}},
{"category":{"id":"4d9c5dfc8ddfd90828000023","description":"Dogs"}},
...
{"category":{"id":"4d9c5dfc8ddfd90828000024","description":"Mules"}}]
Consuming the service with:
public List<Category> getAnimalCategories(Cookie cookie) {
Client client = Client.create(new DefaultClientConfig());
ClientResponse response = client
.resource(Constants.BASE_URL)
.path(Constants.CATEGORIES_ANIMALS)
.accept(MediaType.APPLICATION_JSON)
.type(MediaType.APPLICATION_JSON)
.cookie(cookie)
.get(ClientResponse.class);
return response.getEntity(new GenericType<List<Category>>(){});
}
Where Category.java is:
public class Category {
public String id;
public String description;
public Category() {
}
public Category(String id, String description) {
super();
this.id = id;
this.description = description;
}
The service uses cookie based authentication - that part works and I have other service calls working with the cookie.
Used the Jackson 1.9.6 lib to resolve the issue - see the 2nd line below:
ClientConfig clientConfig = new DefaultClientConfig();
clientConfig.getClasses().add(JacksonJsonProvider.class);
Client client = Client.create(clientConfig);
return client
.resource(Constants.BASE_URL)
.path(Constants.CATEGORIES_ANIMALS)
.type(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.cookie(cookie)
.get(new GenericType<List<AnimalCategoryResponse>>(){});
Also needed to use a new response class:
public class AnimalCategoryResponse {
public Category[] category;
public AnimalCategoryReponse() { }
}

Categories

Resources