JSON deserialisation failing (HTTP 400) with Jersey / MOXy - java

I have a relatively simple RESTful web service which uses Jersey and Eclipselink MOXy.
I can POST requests to the service if the data is formatted as XML, but if I send it as JSON instead, the server generates an HTTP 400 (Bad Request), with the message: "The request sent by the client was syntactically incorrect.".
The service-side looks like this:
#POST
#Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
#Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public Subscription post(Subscription Subscription) {
return Subscriptions.addSubscription(Subscription);
}
If I send it XML data from Javascript in a webpage like this, there is no problem:
$.ajax({
url: 'http://localhost:8080/MyService/subscription',
type: 'POST',
data: "<subscription><notificationType>EMAIL</notificationType><notificationAddress>test#example.com</notificationAddress></subscription>",
headers: {
Accept : "application/xml",
"Content-Type": "application/xml"
},
// .. other params ...
);
However, with the equivalent JSON I get HTTP 400 - Bad Request:
$.ajax({
url: 'http://localhost:8080/MyService/subscription',
type: 'POST',
data: JSON.stringify(
{subscription:{notificationType:"EMAIL",notificationAddress:"test#example.com"}}
),
dataType: 'json'
headers: {
Accept : "application/json",
"Content-Type": "application/json"
}
// .. other params ...
);
I have inspected the request using Fiddler, and the data formatting and headers all look correct.
The interesting thing is that I can successfully unmarshall the exact same JSON string if I plug it into this code:
String json = "{\"subscription\":{\"notificationType\":\"EMAIL\",\"notificationAddress\":\"test#example.com\"}}";
JAXBContext context = JAXBContext.newInstance(Subscription.class);
Unmarshaller m = context.createUnmarshaller();
m.setProperty("eclipselink.media-type", "application/json");
StringReader sr = new StringReader(json);
Subscription sub = (Subscription)m.unmarshal(sr);
System.out.println(sub.toString());
The subscription class is defined as:
#XmlRootElement(name="subscription")
public class Subscription {
public enum NotificationType { EMAIL, SMS };
private String notificationAddress;
private NotificationType notificationType;
public String getNotificationAddress() {
return notificationAddress;
}
public void setNotificationAddress(String notificationAddress) {
this.notificationAddress = notificationAddress;
}
public NotificationType getNotificationType() {
return notificationType;
}
public void setNotificationType(NotificationType notificationType) {
this.notificationType = notificationType;
}
public Subscription() {
}
#Override
public String toString() {
String s = "Subscription";
if (getNotificationAddress() != null) {
s += "(" + getNotificationType().toString() + ":" + getNotificationAddress() + ")";
}
return s;
}
}
I have configured Eclipselink MOXy as my JAXB provider by adding this line to jaxb.properties in the package that contains my Subscriber class:
javax.xml.bind.context.factory=org.eclipse.persistence.jaxb.JAXBContextFactory
and that seems to work, at least for marshalling objects going out from the service.
What am I missing?
EDIT:
This is what Fiddler captures when I post the JSON data:
POST http://localhost:8080/MyService/subscription HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Content-Length: 86
Accept: application/json
Origin: http://localhost:8080
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.116 Safari/537.36
Content-Type: application/json
Referer: http://localhost:8080/TestPage/AddSubscription.html
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-GB,en-US;q=0.8,en;q=0.6
{"subscription":{"notificationType":"EMAIL","notificationAddress":"test#example.com"}}
UPDATE:
I took Option#2 from Blaise's answer below, created an Application-derived class, thus:
import java.util.*;
import javax.ws.rs.core.Application;
import org.eclipse.persistence.jaxb.rs.MOXyJsonProvider;
public class MyApplication extends Application {
#Override
public Set<Class<?>> getClasses() {
HashSet<Class<?>> set = new HashSet<Class<?>>(2);
set.add(MOXyJsonProvider.class);
set.add(SubscriptionResource.class); // the class containing my #POST service method.
return set;
}
}
And added to web.xml:
<param-name>javax.ws.rs.Application</param-name>
<param-value>com.example.MyApplication</param-value>
And now I don't get an HTTP 400, and the code in my service is hit, which it wasn't before, however the passed-in Subscription object has all uninitialized fields, e.g. notificationAddress is null.
If I post using XML, it still works ok.
UPDATE#2:
I have reduced my code to the smallest subset that demonstrates the problem, and you can get it here:
https://www.dropbox.com/sh/2a6iqw65ey0ahrk/D2ILi_722z
The above link contains a .zip with 2 Eclipse projects; TestService (the Jersey RESTful service that accepts a Subscription object) and TestPage (a .html page with some JavaScript to POST a subscription object in either JSON or XML).
If you put a breakpoint in the post method of the service, and use the test page to send the JSON request, you'll see an un-initialised Subscription object gets passed in.

EclipseLink JAXB (MOXy) can be used with Jersey in a couple of different ways to produce JSON.
Option #1 - Add a jaxb.properties File
Jersey can leverage a JAXB (JSR-222) implementation to produced JSON. If you add a jaxb.properties in with your domain model then you can specify MOXy as that provider. For a while the following bug existed in Jersey that prevented MOXy from being used in this way which may be what you are hitting now.
https://java.net/jira/browse/JERSEY-753
This method of producing JSON has some limitations and may not be what your ultimately want.
Option #2 - Leverage MOXyJsonProvider
As of EclipseLink 2.4.0 MOXy offers its own JSON-binding based on the JAXB and MOXy metadata. You can configure this using the MOXyJsonProvider.
import java.util.*;
import javax.ws.rs.core.Application;
import org.eclipse.persistence.jaxb.rs.MOXyJsonProvider;
public class MyApplication extends Application {
#Override
public Set<Class<?>> getClasses() {
HashSet<Class<?>> set = new HashSet<Class<?>>(1);
set.add(SubscriptionResource.class);
return set;
}
#Override
public Set<Object> getSingletons() {
MOXyJsonProvider moxyJsonProvider = new MOXyJsonProvider();
moxyJsonProvider.setIncludeRoot(true);
HashSet<Object> set = new HashSet<Object>(1);
set.add(moxyJsonProvider);
return set;
}
}
For More Information
http://blog.bdoughan.com/2012/05/moxy-as-your-jax-rs-json-provider.html
This is the way I would recommend using MOXy with Jersey, or any other JAX-RS provider.

Related

Mapping incoming JSON to a class in spring boot

I'm struggling to understand why I'm getting the following error when I call my spring boot end point
{
"timestamp": 1489573322678,
"status": 406,
"error": "Not Acceptable",
"exception": "org.springframework.web.HttpMediaTypeNotAcceptableException",
"message": "Could not find acceptable representation",
"path": "/quotes"
}
This is the request that I'm sending to the server
POST /quotes HTTP/1.1
Host: localhost:8080
tamid: 5
Content-Type: application/json
Cache-Control: no-cache
Postman-Token: 94370a3f-6165-106f-f27f-44a44093e0d5
{
"test": "works"
}
I would like the incoming JSON request body to map to a java class I have defined. Here is the class.
#Embedded
public class QuoteVersion {
private String test;
public String getTest() {
return test;
}
public void setTest(String test) {
this.test = test;
}
public void validate() {
}
}
I'm using the #Embedded annotation for use with a mongodb mapping library that I'm hoping is unrelated to the issue I'm facing
Here is the controller method
#RequestMapping(
path = "/quotes",
method = RequestMethod.POST,
headers = "Accept=application/json",
produces = "application/json"
)
public #ResponseBody QuoteStatus create (#RequestHeader(value = "tamid") String tamId,
#RequestBody QuoteVersion firstQuoteVersion) {
// final QuoteVersion firstQuoteVersion = this.quoteFactory.createQuoteVersion(incomingQuote);
final User currentUser = User.getFromTamId(tamId);
currentUser.can(Permissions.CREATE_QUOTE);
firstQuoteVersion.validate();
final Quote newQuote = new Quote();
newQuote.addVersion(firstQuoteVersion);
this.dataRepository.save(newQuote);
QuoteStatus qs = new QuoteStatus(newQuote);
return qs;
}
I'm guessing that Spring Boot for some reason does not understand how to map the incoming payload to the class I have defined but I have no idea how to fix the issue. Thanks in advance for any help you may have to offer.
Spring clearly indicates this problem:
HttpMediaTypeNotAcceptableException
This means that in your content-type header you provided the wrong information or made a syntactical mistake. Try putting there something like application/json.
Also
Make sure the other end will accept it. You currently only accepting requests with an accept header with value application/json. I don't think that is what you want.
So either remove that requirement or add this header to the request.

Send JSON body but with ContentType=application/x-www-form-urlencoded with ClientBuilder

I know the question is weird. Unfortunately I have a service that requires everything to have the header ContentType=application/x-www-form-urlencoded, eventhough the body is JSON
I am trying to use JAX-RS 2.0 ClientBuilder to call it:
String baseUrl = "http://api.example.com/";
JSONObject body = new JSONObject();
body.put("key", "value");
Client client = ClientBuilder.newClient();
client.register(new LoggingFilter());
Builder builder = client.target(baseUrl).path("something").request();
Invocation inv = builder
.header("Content-type", MediaType.APPLICATION_FORM_URLENCODED)
.buildPost(Entity.json(body));
Response response = inv.invoke();
int status = response.getStatus();
// I get 415, unsupported media type (in this case is unexpected)
I have checked my logs and I eventhough I am setting application/x-www-form-urlencoded (via the MediaType) the request appearantly has the Content-type of application/json
How can I force the request to have the Content-type I want?
BTW: This is my custom logger:
public class LoggingFilter implements ClientRequestFilter {
private static final Logger LOG = Logger.getLogger(LoggingFilter.class.getName());
#Override
public void filter(ClientRequestContext requestContext) throws IOException {
LOG.log(Level.INFO, "body");
LOG.log(Level.INFO, requestContext.getEntity().toString());
LOG.log(Level.INFO, "headers");
LOG.log(Level.INFO, requestContext.getHeaders().toString());
}
}
And these are the logs I get:
com.acme.LoggingFilter I body
com.acme.LoggingFilter I {"key":"value"}
com.acme.LoggingFilter I headers
com.acme.LoggingFilter I {Content-type=[application/json]}
The problem with trying to use one of the static Entity helper methods is that it overrides any previous Content-Type header you may have set. In your current case, Entity.json automatically sets the header to application/json.
Instead of using the .json method, you can just use the general purpose Entity.entity(Object, MediaType) method. With your current case though, you can just do Entity.entity(body, MediaType.APPLICATION_FORM_URLENCODED_TYPE) though. The reason is that the client will look for a provider that knows how to serialize a JSONObject to application/x-www-form-urlencoded data, which there is none. So you will need to first serialize it to a String. That way the provider that handles application/x-www-form-urlencoded doesn't need to serialize anything. So just do
Entity.entity(body.toString(), MediaType.APPLICATION_FORM_URLENCODED_TYPE);

Jersey 2.0 Content-Length not set

I'm trying to post to a web service that requires the Content-Length header to be set using the following code:
// EDIT: added apache connector code
ClientConfig clientConfig = new ClientConfig();
ApacheConnector apache = new ApacheConnector(clientConfig);
// setup client to log requests and responses and their entities
client.register(new LoggingFilter(Logger.getLogger("com.example.app"), true));
Part part = new Part("123");
WebTarget target = client.target("https://api.thing.com/v1.0/thing/{thingId}");
Response jsonResponse = target.resolveTemplate("thingId", "abcdefg")
.request(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, "anauthcodehere")
.post(Entity.json(part));
From the release notes https://java.net/jira/browse/JERSEY-1617 and the Jersey 2.0 documentation https://jersey.java.net/documentation/latest/message-body-workers.html it implies that Content-Length is automatically set. However, I get a 411 response code back from the server indicating that Content-Length is not present in the request.
Does anyone know the best way to get the Content-Length header set?
I've verified through setting up a logger that the Content-Length header is not generated in the request.
Thanks.
I ran a quick test with Jersey Client 2.2 and Netcat, and it is showing me that Jersey is sending the Content-Length header, even though the LoggingFilter is not reporting it.
To do this test, I first ran netcat in one shell.
nc -l 8090
Then I executed the following Jersey code in another shell.
Response response = ClientBuilder.newClient()
.register(new LoggingFilter(Logger.getLogger("com.example.app"), true))
.target("http://localhost:8090/test")
.request()
.post(Entity.json(IOUtils.toInputStream("{key:\"value\"}")));
After running this code, the following lines get logged.
INFO: 1 * LoggingFilter - Request received on thread main
1 > POST http://localhost:8090/test
1 > Content-Type: application/json
{key:"value"}
However, netcat reports several more headers in the message.
POST /test HTTP/1.1
Content-Type: application/json
User-Agent: Jersey/2.0 (HttpUrlConnection 1.7.0_17)
Host: localhost:8090
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Connection: keep-alive
Content-Length: 13
{key:"value"}
I ran this test on OSX with Java6 and Java7, with the same results. I also ran the test in Jersey 2.0, with similar results.
After looking at the source code for the ApacheConnector class, I see the problem. When a ClientRequest is converted to a HttpUriRequest a private method getHttpEntity() is called that returns a HttpEntity. Unfortunately, this returns a HttpEntity whose getContentLength() always returns a -1.
When the Apache http client creates the request it will consult the HttpEntity object for a length and since it returns -1 no Content-Length header will be set.
I solved my problem by creating a new connector that is a copy of the source code for the ApacheConnector but has a different implementation of the getHttpEntity(). I read the entity from the original ClientRequest into a byte array and then wrap that byte array with a ByteArrayEntity. When the Apache Http client creates the request it will consult the entity and the ByteArrayEntity will respond with the correct content length which in turns allows the Content-Length header to be set.
Here's the relevant code:
private HttpEntity getHttpEntity(final ClientRequest clientRequest) {
final Object entity = clientRequest.getEntity();
if (entity == null) {
return null;
}
byte[] content = getEntityContent(clientRequest);
return new ByteArrayEntity(content);
}
private byte[] getEntityContent(final ClientRequest clientRequest) {
// buffer into which entity will be serialized
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
// set up a mock output stream to capture the output
clientRequest.setStreamProvider(new OutboundMessageContext.StreamProvider() {
#Override
public OutputStream getOutputStream(int contentLength) throws IOException {
return baos;
}
});
try {
clientRequest.writeEntity();
}
catch (IOException e) {
LOGGER.log(Level.SEVERE, null, e);
// re-throw new exception
throw new ProcessingException(e);
}
return baos.toByteArray();
}
WARNING: My problem space was constrained and only contained small entity bodies as part of requests. This method proposed above may be problematic with large entity bodies such as images so I don't think this is a general solution for all.
I've tested with Jersey 2.25.1 a simpler solution that consists in setting setChunkedEncodingEnabled(false) in the Jersey Client configuration. Instead of using a chunked encoding, the whole entity is serialised in memory and the Content-Length is set on the request.
For reference, here is an example of a configuration I've used:
private Client createJerseyClient(Environment environment) {
Logger logger = Logger.getLogger(getClass().getName());
JerseyClientConfiguration clientConfig = new JerseyClientConfiguration();
clientConfig.setProxyConfiguration(new ProxyConfiguration("localhost", 3333));
clientConfig.setGzipEnabled(false);
clientConfig.setGzipEnabledForRequests(false);
clientConfig.setChunkedEncodingEnabled(false);
return new JerseyClientBuilder(environment)
.using(clientConfig)
.build("RestClient")
.register(new LoggingFeature(logger, Level.INFO, null, null));
}
I've used mitmproxy to verify the request headers and the Content-Length header was set correctly.
This is supported in Jersey 2.5 (https://java.net/jira/browse/JERSEY-2224). You could use https://jersey.java.net/apidocs/latest/jersey/org/glassfish/jersey/client/RequestEntityProcessing.html#BUFFERED to stream your content. I put together a simple example that shows both chunked and buffering content using ApacheConnector. Checkout this project: https://github.com/aruld/sof-18157218
public class EntityStreamingTest extends JerseyTest {
private static final Logger LOGGER = Logger.getLogger(EntityStreamingTest.class.getName());
#Path("/test")
public static class HttpMethodResource {
#POST
#Path("chunked")
public String postChunked(#HeaderParam("Transfer-Encoding") String transferEncoding, String entity) {
assertEquals("POST", entity);
assertEquals("chunked", transferEncoding);
return entity;
}
#POST
public String postBuffering(#HeaderParam("Content-Length") String contentLength, String entity) {
assertEquals("POST", entity);
assertEquals(entity.length(), Integer.parseInt(contentLength));
return entity;
}
}
#Override
protected Application configure() {
ResourceConfig config = new ResourceConfig(HttpMethodResource.class);
config.register(new LoggingFilter(LOGGER, true));
return config;
}
#Override
protected void configureClient(ClientConfig config) {
config.connectorProvider(new ApacheConnectorProvider());
}
#Test
public void testPostChunked() {
Response response = target().path("test/chunked").request().post(Entity.text("POST"));
assertEquals(200, response.getStatus());
assertTrue(response.hasEntity());
}
#Test
public void testPostBuffering() {
ClientConfig cc = new ClientConfig();
cc.property(ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.BUFFERED);
cc.connectorProvider(new ApacheConnectorProvider());
JerseyClient client = JerseyClientBuilder.createClient(cc);
WebTarget target = client.target(getBaseUri());
Response response = target.path("test").request().post(Entity.text("POST"));
assertEquals(200, response.getStatus());
assertTrue(response.hasEntity());
}
}
#Test
public void testForbiddenHeadersAllowed() {
Client client = ClientBuilder.newClient();
System.setProperty("sun.net.http.allowRestrictedHeaders", "true");
Response response = testHeaders(client);
System.out.println(response.readEntity(String.class));
Assert.assertEquals(200, response.getStatus());

Issue with POST JSON to a Jersey REST service

I have a problem with posting JSON to a Jersey REST service - GET is working perfectly but POST seems tricky. I've been working on this problem for awhile now, with no solution so far. Any help is much appreciated!
It seems it cant find the U RL to send the json?Here is what FireBug console shows:
POST http://localhost:9998/data 400 Bad Request
Post source: name=Tony
**Response Headers**
Connection close
Content-Length 0
Content-Type text/html; charset=iso-8859-1
Date Fri, 20 Apr 2012 10:13:24 GMT
**Request Headers**
Accept application/json, text/javascript, */*; q=0.01
Accept-Encoding gzip, deflate
Accept-Language sv-se,sv;q=0.8,en-us;q=0.5,en;q=0.3
Connection keep-alive
Content-Length 9
Content-Type application/json; charset=UTF-8
Host localhost:9998
Referer http://localhost:9998/static/page.html
User-Agent Mozilla/5.0 (Windows NT 6.1; WOW64; rv:11.0) Gecko/20100101 Firefox/11.0
X-Requested-With XMLHttpRequest
I'm doing the POST as follows:
<button id='btn' value="knapp" name="knapp" />
<script type="text/javascript">
$('#btn').click(function(){
$.ajax({
url: '/data',
type: 'POST',
contentType: 'application/json',
data: {name:"Tony"},
dataType: 'json'
});
})
</script>
Javabean class with #XmlRootElement:
#XmlRootElement
public class StatusBean {
private String name;
public StatusBean() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Resource method:
#Path("/data")
public class PostData {
#POST
#Consumes(MediaType.APPLICATION_JSON)
#Produces(MediaType.APPLICATION_JSON)
public StatusBean post(StatusBean sb) {
System.out.println(sb);
return sb;
}
}
The server, set up with Grizzly:
public class Main {
public static final URI BASE_URI = getBaseURI();
public static void main(String[] args) throws IOException {
HttpServer httpServer = startServer();
Map<String,String> initParams = new HashMap<String, String>();
initParams.put("com.sun.jersey.config.property.packages", "server");
SelectorThread selector = GrizzlyWebContainerFactory.create("http://localhost:9998/", initParams );
System.out.println(String.format("Jersey app started with WADL available at "
+ "%sapplication.wadl\nTry out %shelloworld\nHit enter to stop it...",
BASE_URI, BASE_URI));
System.in.read();
httpServer.stop();
}
protected static HttpServer startServer() throws IOException {
System.out.println("Starting grizzly...");
ClassNamesResourceConfig rc = new ClassNamesResourceConfig(PostData.class);
// rc.getFeatures().put(JSONConfiguration.FEATURE_POJO_MAPPING, true);
HttpServer server = GrizzlyServerFactory.createHttpServer(BASE_URI, rc);
server.getServerConfiguration().addHttpHandler(new StaticHttpHandler(new File(".").getAbsolutePath()), "/static");
return server;
}
private static int getPort(int defaultPort) {
String port = System.getProperty("jersey.test.port");
if (null != port) {
try {
return Integer.parseInt(port);
} catch (NumberFormatException e) {
}
}
return defaultPort;
}
private static URI getBaseURI() {
return UriBuilder.fromUri("http://localhost/").port(getPort(9998)).build();
}
}
Try making your bean serializable.
#XmlRootElement
public class StatusBean implements Serializable {
....
}
Check your POST url. It should be `
http://localhost:9998/{projectname}/{restservletmapping}/data
For example, if my web.xml looks like this and my project name is SampleProject
<servlet-mapping>
<servlet-name>Jersey REST Service</servlet-name>
<url-pattern>/rest/*</url-pattern>
</servlet-mapping>
URL would be : http://localhost:9998/SampleProject/rest/data
You can use tools for testing REST services like SOAP UI or browser addons like POSTMAN, REST CONSOLE, etc.
If above things are fine and REST service is giving response with testing tools.
Then it could be problem of Cross Origin Policy in ajax.
I had the same problem. The issue is that your data is not converted to JSON string automatically.
So you just need to call JSON.stringify(...) on your data before posting it:
<button id='btn' value="knapp" name="knapp" />
<script type="text/javascript">
$('#btn').click(function(){
$.ajax({
url: '/data',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({name:"Tony"}),
dataType: 'json'
});
})
</script>
This should work.
From your server config I see that you haven't configured JAX-RS with Grizzly. On the base of that example you should somehow pass such property
Map<String,String> initParams = new HashMap<String, String>();
initParams.put( "com.sun.jersey.config.property.packages", "package.with.your.StatusBean.class" );
Another configuration option is to use
ResourceConfig rc = new PackagesResourceConfig("your.package.with.resources");
and start grizzly server:
GrizzlyServerFactory.createHttpServer(BASE_URI, rc);
See details: http://jersey.java.net/nonav/documentation/latest/user-guide.html (Chapter "Deploying the root resource"). Try to run first example they have.
Are you sure that the path you're posting to is complete? You should define another Path annotation on the post method and use that in the URL you're posting to:
#Path("/data")
public class PostData {
#Path("/postStatus")
#POST
#Consumes(MediaType.APPLICATION_JSON)
#Produces(MediaType.APPLICATION_JSON)
public StatusBean post(StatusBean sb) {
System.out.println(sb);
return sb;
}
}
Then use the /data/postStatus path to post your request to:
<button id='btn' value="knapp" name="knapp" />
<script type="text/javascript">
$('#btn').click(function(){
$.ajax({
url: '/data/postStatus',
type: 'POST',
contentType: 'application/json',
data: {name:"Tony"},
dataType: 'json'
});
})
</script>
You have probably forgotten to register the JSON mapper, i.e. Jackson (or whatever mapper you use). The feature is not enabled automatically, you have to load the class in your ResourceConfig:
org.glassfish.jersey.jackson.JacksonFeature.class
sample
also, see JSON howto

JAX-RS Post multiple objects

I have a method;
#POST
#Path("test")
#Consumes(MediaType.APPLICATION_JSON)
public void test(ObjectOne objectOne, ObjectTwo objectTwo)
Now I know I can post a single object in json format, just putting it into the body.
But is it possible to do multiple objects? If so, how?
You can not use your method like this as correctly stated by Tarlog.
However, you can do this:
#POST
#Path("test")
#Consumes(MediaType.APPLICATION_JSON)
public void test(List<ObjectOne> objects)
or this:
#POST
#Path("test")
#Consumes(MediaType.APPLICATION_JSON)
public void test(BeanWithObjectOneAndObjectTwo containerObject)
Furthermore, you can always combine your method with GET parameters:
#POST
#Path("test")
#Consumes(MediaType.APPLICATION_JSON)
public void test(List<ObjectOne> objects, #QueryParam("objectTwoId") long objectTwoId)
The answer is no.
The reason is simple: This about the parameters you can receive in a method. They must be related to the request. Right? So they must be either headers or cookies or query parameters or matrix parameters or path parameters or request body. (Just to tell the complete story there is additional types of parameters called context).
Now, when you receive JSON object in your request, you receive it in a request body. How many bodies the request may have? One and only one. So you can receive only one JSON object.
If we look at what the OP is trying to do, he/she is trying to post two (possibly unrelated) JSON objects. First any solution to try and send one part as the body, and one part as some other param, IMO, are horrible solutions. POST data should go in the body. It's not right to do something just because it works. Some work-arounds might be violating basic REST principles.
I see a few solutions
Use application/x-www-form-urlencoded
Use Multipart
Just wrap them in a single parent object
1. Use application/x-www-form-urlencoded
Another option is to just use application/x-www-form-urlencoded. We can actually have JSON values. For examle
curl -v http://localhost:8080/api/model \
-d 'one={"modelOne":"helloone"}' \
-d 'two={"modelTwo":"hellotwo"}'
public class ModelOne {
public String modelOne;
}
public class ModelTwo {
public String modelTwo;
}
#Path("model")
public class ModelResource {
#POST
#Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public String post(#FormParam("one") ModelOne modelOne,
#FormParam("two") ModelTwo modelTwo) {
return modelOne.modelOne + ":" + modelTwo.modelTwo;
}
}
The one thing we need to get this to work is a ParamConverterProvider to get this to work. Below is one that has been implemented by Michal Gadjos of the Jersey Team (found here with explanation).
#Provider
public class JacksonJsonParamConverterProvider implements ParamConverterProvider {
#Context
private Providers providers;
#Override
public <T> ParamConverter<T> getConverter(final Class<T> rawType,
final Type genericType,
final Annotation[] annotations) {
// Check whether we can convert the given type with Jackson.
final MessageBodyReader<T> mbr = providers.getMessageBodyReader(rawType,
genericType, annotations, MediaType.APPLICATION_JSON_TYPE);
if (mbr == null
|| !mbr.isReadable(rawType, genericType, annotations, MediaType.APPLICATION_JSON_TYPE)) {
return null;
}
// Obtain custom ObjectMapper for special handling.
final ContextResolver<ObjectMapper> contextResolver = providers
.getContextResolver(ObjectMapper.class, MediaType.APPLICATION_JSON_TYPE);
final ObjectMapper mapper = contextResolver != null ?
contextResolver.getContext(rawType) : new ObjectMapper();
// Create ParamConverter.
return new ParamConverter<T>() {
#Override
public T fromString(final String value) {
try {
return mapper.reader(rawType).readValue(value);
} catch (IOException e) {
throw new ProcessingException(e);
}
}
#Override
public String toString(final T value) {
try {
return mapper.writer().writeValueAsString(value);
} catch (JsonProcessingException e) {
throw new ProcessingException(e);
}
}
};
}
}
If you aren't scanning for resource and providers, just register this provider, and the above example should work.
2. Use Multipart
One solution that no one has mentioned, is to use multipart. This allows us to send arbitrary parts in a request. Since each request can only have one entity body, multipart is the work around, as it allows to have different parts (with their own content types) as part of the entity body.
Here is an example using Jersey (see official doc here)
Dependency
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-multipart</artifactId>
<version>${jersey-2.x.version}</version>
</dependency>
Register the MultipartFeature
import javax.ws.rs.ApplicationPath;
import org.glassfish.jersey.media.multipart.MultiPartFeature;
import org.glassfish.jersey.server.ResourceConfig;
#ApplicationPath("/api")
public class JerseyApplication extends ResourceConfig {
public JerseyApplication() {
packages("stackoverflow.jersey");
register(MultiPartFeature.class);
}
}
Resource class
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.glassfish.jersey.media.multipart.FormDataParam;
#Path("foobar")
public class MultipartResource {
#POST
#Consumes(MediaType.MULTIPART_FORM_DATA)
public Response postFooBar(#FormDataParam("foo") Foo foo,
#FormDataParam("bar") Bar bar) {
String response = foo.foo + "; " + bar.bar;
return Response.ok(response).build();
}
public static class Foo {
public String foo;
}
public static class Bar {
public String bar;
}
}
Now the tricky part with some clients is that there isn't a way to set the Content-Type of each body part, which is required for the above to work. The multipart provider will look up message body reader, based on the type of each part. If it's not set to application/json or a type, the Foo or Bar has a reader for, this will fail. We will use JSON here. There's no extra configuration but to have a reader available. I'll use Jackson. With the below dependency, no other configuration should be required, as the provider will be discovered through classpath scanning.
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-jackson</artifactId>
<version>${jersey-2.x.version}</version>
</dependency>
Now the test. I will be using cURL. You can see I explicitly set the Content-Type for each part with type. The -F signifies to different part. (See very bottom of the post for an idea of how the request body actually looks.)
curl -v -X POST \
-H "Content-Type:multipart/form-data" \
-F "bar={\"bar\":\"BarBar\"};type=application/json" \
-F "foo={\"foo\":\"FooFoo\"};type=application/json" \
http://localhost:8080/api/foobar
Result: FooFoo; BarBar
The result is exactly as we expected. If you look at the resource method, all we do is return this string foo.foo + "; " + bar.bar, gathered from the two JSON objects.
You can see some examples using some different JAX-RS clients, in the links below. You will also see some server side example also from those different JAX-RS implementations. Each link should have somewhere in it a link to the official documentation for that implementation
Jersey example
Resteasy example
CXF example
There are other JAX-RS implementations out there, but you will need to find the documentation for it yourself. The above three are the only ones I have experience with.
As far as Javascript clients, most of the example I see (e.g. some of these involve setting the Content-Type to undefined/false (using FormData), letting the Browser handle the it. But this will not work for us, as the Browser will not set the Content-Type for each part. And the default type is text/plain.
I'm sure there are libraries out there that allow setting the type for each part, but just to show you how it can be done manually, I'll post an example (got a little help from here. I'll be using Angular, but the grunt work of building the entity body will be plain old Javascript.
<!DOCTYPE html>
<html ng-app="multipartApp">
<head>
<script src="js/libs/angular.js/angular.js"></script>
<script>
angular.module("multipartApp", [])
.controller("defaultCtrl", function($scope, $http) {
$scope.sendData = function() {
var foo = JSON.stringify({foo: "FooFoo"});
var bar = JSON.stringify({bar: "BarBar"});
var boundary = Math.random().toString().substr(2);
var header = "multipart/form-data; charset=utf-8; boundary=" + boundary;
$http({
url: "/api/foobar",
headers: { "Content-Type": header },
data: createRequest(foo, bar, boundary),
method: "POST"
}).then(function(response) {
$scope.result = response.data;
});
};
function createRequest(foo, bar, boundary) {
var multipart = "";
multipart += "--" + boundary
+ "\r\nContent-Disposition: form-data; name=foo"
+ "\r\nContent-type: application/json"
+ "\r\n\r\n" + foo + "\r\n";
multipart += "--" + boundary
+ "\r\nContent-Disposition: form-data; name=bar"
+ "\r\nContent-type: application/json"
+ "\r\n\r\n" + bar + "\r\n";
multipart += "--" + boundary + "--\r\n";
return multipart;
}
});
</script>
</head>
<body>
<div ng-controller="defaultCtrl">
<button ng-click="sendData()">Send</button>
<p>{{result}}</p>
</div>
</body>
</html>
The interesting part is the createRequest function. This is where we build the multipart, setting the Content-Type of each part to application/json, and concatenating the stringified foo and bar objects to each part. If you are unfamiliar multipart format see here for more info. The other interesting part is the header. We set it to multipart/form-data.
Below is the result. In Angular I just used the result to show in the HTML, with $scope.result = response.data. The button you see was just to make the request. You will also see the request data in firebug
3. Just wrap them in a single parent object
This option should be self explanatory, as others have already mentioned.
The next approach is usually applied in this kind of cases:
TransferObject {
ObjectOne objectOne;
ObjectTwo objectTwo;
//getters/setters
}
#POST
#Path("test")
#Consumes(MediaType.APPLICATION_JSON)
public void test(TransferObject object){
// object.getObejctOne()....
}
You can't put two separate objects in one single POST call as explained by Tarlog.
Anyway you could create a third container object that contains the first two objects and pass that one within the POS call.
I have also faced with these problem. Maybe this will help.
#POST
#Path("/{par}")
#Produces(MediaType.APPLICATION_JSON)
#Consumes(MediaType.APPLICATION_JSON)
public Object centralService(#PathParam("par") String operation, Object requestEntity) throws JSONException {
ObjectMapper objectMapper=new ObjectMapper();
Cars cars = new Cars();
Seller seller = new Seller();
String someThingElse;
HashMap<String, Object> mapper = new HashMap<>(); //Diamond )))
mapper = (HashMap<String, Object>) requestEntity;
cars=objectMapper.convertValue(mapper.get("cars"), Cars.class);
seller=objectMapper.convertValue(mapper.get("seller"), Seller.class);
someThingElse=objectMapper.convertValue(mapper.get("someThingElse"), String.class);
System.out.println("Cars Data "+cars.toString());
System.out.println("Sellers Data "+seller.toString());
System.out.println("SomeThingElse "+someThingElse);
if (operation.equals("search")) {
System.out.println("Searching");
} else if (operation.equals("insertNewData")) {
System.out.println("Inserting New Data");
} else if (operation.equals("buyCar")) {
System.out.println("Buying new Car");
}
JSONObject json=new JSONObject();
json.put("result","Works Fine!!!");
return json.toString();
}
*******CARS POJO********#XmlRootElement for Mapping Object to XML or JSON***
#XmlRootElement
public class Cars {
private int id;
private String brand;
private String model;
private String body_type;
private String fuel;
private String engine_volume;
private String horsepower;
private String transmission;
private String drive;
private String status;
private String mileage;
private String price;
private String description;
private String picture;
private String fk_seller_oid;
} // Setters and Getters Omitted
*******SELLER POJO********#XmlRootElement for Mapping Object to XML or JSON***
#XmlRootElement
public class Seller {
private int id;
private String name;
private String surname;
private String phone;
private String email;
private String country;
private String city;
private String paste_date;
}//Setters and Getters omitted too
*********************FRONT END Looks Like This******************
$(function(){
$('#post').on('click',function(){
console.log('Begins');
$.ajax({
type:'POST',
url: '/ENGINE/cars/test',
contentType: "application/json; charset=utf-8",
dataType: "json",
data:complexObject(),
success: function(data){
console.log('Sended and returned'+JSON.stringify(data));
},
error: function(err){
console.log('Error');
console.log("AJAX error in request: " + JSON.stringify(err, null, 2));
}
}); //-- END of Ajax
console.log('Ends POST');
console.log(formToJSON());
}); // -- END of click function POST
function complexObject(){
return JSON.stringify({
"cars":{"id":"1234","brand":"Mercedes","model":"S class","body_type":"Sedan","fuel":"Benzoline","engine_volume":"6.5",
"horsepower":"1600","transmission":"Automat","drive":"Full PLag","status":"new","mileage":"7.00","price":"15000",
"description":"new car and very nice car","picture":"mercedes.jpg","fk_seller_oid":"1234444"},
"seller":{ "id":"234","name":"Djonotan","surname":"Klinton","phone":"+994707702747","email":"email#gmail.com", "country":"Azeribaijan","city":"Baku","paste_date":"20150327"},
"someThingElse":"String type of element"
});
} //-- END of Complex Object
});// -- END of JQuery - Ajax
It can be done by having the POST method declared to accept array of objects. Example like this
T[] create(#RequestBody T[] objects) {
for( T object : objects ) {
service.create(object);
}
}
Change #Consumes(MediaType.APPLICATION_JSON)
to #Consumes({MediaType.APPLICATION_FORM_URLENCODED})
Then you can putting multiple objects into the body
My solution is written for CXF, it appears to be quite simple.
import org.apache.cxf.jaxrs.ext.multipart.Multipart;
#POST
#Path("paramTest")
#Consumes(MediaType.MULTIPART_FORM_DATA)
public GenericResult paramTest(
#Multipart(value = "myData", type=MediaType.APPLICATION_JSON)
ObjectOne myData,
#Multipart(value = "infoList", type=MediaType.APPLICATION_JSON)
ObjectTwo[] infoList);
The test code for this with io.restassurred:
#Test
public void paramTest()
{
String payload1 = "" +
"{ \"name\": \"someName\", \"branch\": \"testBranch\" }";
String payload2 =
" [ { \"name\": \"cn\", \"status\": \"ts\" }," +
"{ \"name\": \"cn2\", \"status\": \"ts2\" } ] ]";
RestAssured.
given().
contentType("multipart/form-data").
multiPart("myData", payload1, "application/json").
multiPart("infoList", payload2, "application/json").
post(String.format("%s/paramTest", API_PATH)).
then().
statusCode(HttpStatus.SC_OK).
contentType(ContentType.JSON).
body("success", Matchers.equalTo(true));
}

Categories