ResponseExceptionMapper in cxf using Client (javax.ws.rs.client.Client) API - exception

This post does not resolve the issue: ResponseExceptionMapper in cxf client . You will notice that I did in fact register and annotate my Provider, and I tried with WebApplicationException as suggested instead of Exception/CustomException.
Problem Statement: Unable to implement custom client side exception handler using Client (javax.ws.rs.client.Client) API, and #Provider class implementing the ResponseExceptionMapper interface.
Questions:
Does Client API not support custom client side providers for exception handling?
Any literature I looked up for this problem statement uses JAXRSClientFactory implementation; I'm yet to find any using Client API for this scenario. Would I have to switch my implementation?
What is the difference between Client API and JAXRSClientFactory implementations?
I am working on a cxf Client API implementation in Java, and noticed that for http status codes above 300 cxf wraps the Response in either a WebApplicationException or ProcessingException (depending upon the response status code). The server in my case has a customized response body indicating the actual reason for an http status code !200, like below (for response code = 412):
{
"requestError": {
"serviceException": {
"messageId": "SVC4120",
"text": "Invalid Request: Invalid Coupon Code."
}
}
}
Unfortunately the WebApplicationException itself does not render this. Instead the only message captured in the exception directly is a generic "412 Precondition Failed". I can do something similar to below exception block from code snippet (includes Client API code snippet):
protected RESPOBJ invoke(String endPointUrl) throws CustomException {
Object reqPOJO = prepareRequest();
try {
if(client == null) {
ClientBuilder builder = ClientBuilder.newBuilder();
//register custom JAX-RS components
builder.register(new CustomMapper());
}
WebTarget target = client.target(endPointUrl);
//do this when queryParams exist
if(!getUriParams().isEmpty()) {
for(Map.Entry<String, String> queryParams : getUriParams().entrySet()) {
target = target.queryParam(queryParams.getKey(), queryParams.getValue());
}
}
Invocation.Builder builder = target.request();
//create headers here
MultivaluedMap<String, Object> headers = new MultivaluedHashMap<>();
if(isBasicAuthRequired()) {
headers.add(AUTH_HEADER_PARAM, getBasicAuthentication());
}
headers.add(CONTENT_TYPE, getMediaType().toString());
builder.headers(headers);
builder.accept(getMediaType().toString());
//GET or POST
if(HttpMethodType.GET.equals(getHttpMethod())) {
return builder.get(RESPOBJ.class);
}
return builder.post(Entity.entity(reqPOJO, getMediaType()), RESPOBJ.class);
}
catch (Exception ex) {
if(ex instanceof ResponseProcessingException) {
ResponseProcessingException e = (ResponseProcessingException) ex;
logger.error("Unmarshalling failed: [" + e.getResponse().readEntity(String.class) + "]");
}
else if(ex instanceof WebApplicationException) {
WebApplicationException e = (WebApplicationException) ex;
logger.error("Error Response: ["+e.getResponse().readEntity(String.class) + "]");
}
throw new CustomException(ex);
}
}
However, I am looking to implement something cleaner, preferably using a custom Exception handler that implements ResponseExceptionMapper<> interface. From literature I noticed the only implementations of ResponseExceptionMapper for custom client side exception handling are using JAXRSClientFactory. My current implementation however uses the Client API (code snippet below). From a design aspect I will modify this to have a separate CustomExceptionMapper class that would be the Provider only for Exception cases, but I do not see why this Custom class is registered as a Provider (works for 200 status codes as MBR, and the MBW works always) but does not work for exception cases.
Update: While debugging and observing changes between a 200 vs >300 status code (412 in my case), I noticed that for 200 case JAXRSUtils.readFromMessageBodyReader() method gets invoked, which for the 1st time retrieves the Custom Provider. The code never gets here for status codes shown below in code snippet which should be the reason for not finding the CustomMapper. Is there any difference in how I must register my CustomExceptionMapper? Or does the Client API simply not support this functionality?
// for failure case the method above returns null (status > 300), whereas for success 200 case it executes method in last line and gets the provider.
// AbstractClient class that invokes the doReadEntity() method which in turn invokes and finds the Provider in JAXRSUtils.readFromMessageBodyReader() method code
protected <T> T readBody(Response r, Message outMessage, Class<T> cls,
Type type, Annotation[] anns) {
if (cls == Response.class) {
return cls.cast(r);
}
int status = r.getStatus();
//this is invoked for failure case
if ((status < 200 || status == 204) && r.getLength() <= 0 || status >= 300) {
return null;
}
//this for 200 status code
return ((ResponseImpl)r).doReadEntity(cls, type, anns);
}
//My custom provider code
#Provider
#Consumes
#Produces(MediaType.APPLICATION_JSON)
public class CustomMapper implements MessageBodyReader<CustomResponse>, MessageBodyWriter<CustomRequest>, ResponseExceptionMapper<CustomException> {
private Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create();
#Override
public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
return type.isAssignableFrom(CustomResponse.class);
}
#Override
public CustomResponse readFrom(Class<CustomResponse> type, Type genericType, Annotation[] annotations,
MediaType mediaType, MultivaluedMap<String, String> httpHeaders, InputStream entityStream) throws IOException, WebApplicationException {
CustomResponse respObj = new CustomResponse();
//json to pojo code
return respObj;
}
#Override
public long getSize(CustomRequest reqObj, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
return -1;
}
#Override
public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
return type.isAssignableFrom(CustomRequest.class);
}
#Override
public void writeTo(CustomRequest reqObj, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException {
entityStream.write(gson.toJson(reqObj).getBytes());
}
#Override
public CustomException fromResponse(Response exceptionResponse) {
//Response obj to my CustomException code
return (CustomException);
}
}
Questions:
I'm trying to figure out what is done wrong here, and if Client API does not support custom client side exception handling for any reason?
What is the difference between Client API and JAXRSClientFactory implementations?
I also am looking into possibly using ClientResponseFilter (haven't tried this yet).
Any help appreciated. Thanks.

Related

How to properly send Mono<ResponseEntity> as JSON in Netty Reactor HTTP Server response

I am trying to figure out how to properly send response with ResponseEntity as JSON from Netty Reactor HTTP Server.
My current implementation reacts on request from WebClient and should send back response with some ResponseEntity status (let's assume just HTTP OK).
Unfortunately I'm still getting InvalidDefinitionException on the client side saying that it not possible to construct instance due to no default constructor.
I know what it means but for example Spring Webflux can have return type of rest endpoint Mono as well and no issues on client side will appear.
So is it somehow possible to properly serialize entity as JSON on server side and deserialize it at the client side?
This is my client
import org.springframework.web.reactive.function.client.WebClient;
public Mono<ResponseEntity> postRequest(final Object body, final String uri) {
return webClient.post()
.uri(uri)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(body))
.exchange()
.flatMap(clientResponse -> clientResponse.toEntity(ResponseEntity.class));
}
This is my Server
public void runWithPost(final String endpointPath, final ServerCallback callback) {
server = HttpServer.create()
.host(this.host)
.port(this.port)
.route(routes ->
routes.post(endpointPath, (request, response) ->
response.addHeader(CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.sendString(Mono.just(getJSON(callback.handleCallback())))))
.wiretap(true)
.bindNow();
System.out.println("Starting server...");
}
private String getJSON(final ResponseEntity responseEntity) {
String json = StringUtils.EMPTY;
try {
json = objectMapper.writeValueAsString(responseEntity);
System.out.println("Serialized JSON: " + json);
} catch (final JsonProcessingException ex) {
System.err.println("JSON serializer error: " + ex.getMessage());
}
return json;
}
This is callback
public interface ServerCallback {
ResponseEntity handleCallback();
}
and usage
reactiveRestServer.runWithPost("/transaction", () -> ResponseEntity.ok().build());
Unfortunately on the client side I do not get HTTP status OK but deserialization exception:
2020-04-28 16:09:35.345 ERROR 15136 --- [ctor-http-nio-2] c.a.t.t.inbound.ArpMessageServiceImpl : Type definition error: [simple type, class org.springframework.http.ResponseEntity]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `org.springframework.http.ResponseEntity` (no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
at [Source: (io.netty.buffer.ByteBufInputStream); line: 1, column: 2]
2020-04-28 16:09:35.349 WARN 15136 --- [ctor-http-nio-2] io.netty.util.ReferenceCountUtil : Failed to release a message: DefaultLastHttpContent(data: PooledSlicedByteBuf(freed), decoderResult: success)
io.netty.util.IllegalReferenceCountException: refCnt: 0, decrement: 1
at io.netty.util.internal.ReferenceCountUpdater.toLiveRealRefCnt(ReferenceCountUpdater.java:74) ~[netty-common-4.1.45.Final.jar:4.1.45.Final]
What I am missing?
So I finally resolved that issue. For those who would be solving similar issue here is the answer.
The problem is that Spring Webflux converts ResponseEntity into DefaultFullHttpResponse, so that DefaultFullHttpResponse contains headers, status and also body. I resolved that issue by doing exactly the same approach.
public void runWithPost(final String endpointPath, final ServerCallback callback) {
if (server == null || server.isDisposed()) {
server = HttpServer.create()
.host(this.host)
.port(this.port)
.route(routes ->
routes.post(endpointPath, (request, response) -> processResponse(response, callback)))
.wiretap(true)
.bindNow();
logger.info("Starting server...");
} else {
logger.info("Couldn't start server because one is already running!");
}
}
and conversion is here
private NettyOutbound processResponse(final HttpServerResponse response, final ServerCallback callback) {
final ResponseEntity responseEntity = callback.handleCallback();
// set status
response.status(responseEntity.getStatusCodeValue());
// set headers
final HttpHeaders entityHeaders = responseEntity.getHeaders();
if (!entityHeaders.isEmpty()) {
entityHeaders.entrySet().stream()
.forEach(entry -> response.addHeader(entry.getKey(), buildValue(entry.getValue())));
}
if (responseEntity.hasBody()) {
try {
final Object body = responseEntity.getBody();
if (body instanceof String) {
return response.sendString(Mono.just((String) body));
} else {
return response.send(Mono.just(Unpooled.wrappedBuffer(getBytesFromObject(body))));
}
} catch (final IOException ex) {
response.status(HttpResponseStatus.INTERNAL_SERVER_ERROR);
return response.sendString(Mono.just(ex.getMessage()));
}
}
// set body
return response.send(Mono.empty());
}
Usage is as follows:
mockReactiveRestServer.runWithPost("/transaction", () -> ResponseEntity.ok().build());

Spring AMQP RPC consumer and throw exception

I have a consumer (RabbitListner) in RPC mode and I would like to know if it is possible to throw exception that can be treated by the publisher.
To make more clear my explication the case is as follow :
The publisher send a message in RPC mode
The consumer receive the message, check the validity of the message and if the message can not be take in count, because of missing parameters, then I would like to throw Exception. The exception can be a specific business exception or a particular AmqpException but I want that the publisher can handle this exception if it is not go in timeout.
I try with the AmqpRejectAndDontRequeueException, but my publisher do not receive the exception, but just a response which is empty.
Is it possible to be done or may be it is not a good practice to implement like that ?
EDIT 1 :
After the #GaryRussel response here is the resolution of my question:
For the RabbitListner I create an error handler :
#Configuration
public class RabbitErrorHandler implements RabbitListenerErrorHandler {
#Override public Object handleError(Message message, org.springframework.messaging.Message<?> message1, ListenerExecutionFailedException e) {
throw e;
}
}
Define the bean into a configuration file :
#Configuration
public class RabbitConfig extends RabbitConfiguration {
#Bean
public RabbitTemplate getRabbitTemplate() {
Message.addWhiteListPatterns(RabbitConstants.CLASSES_TO_SEND_OVER_RABBITMQ);
return new RabbitTemplate(this.connectionFactory());
}
/**
* Define the RabbitErrorHandle
* #return Initialize RabbitErrorHandle bean
*/
#Bean
public RabbitErrorHandler rabbitErrorHandler() {
return new RabbitErrorHandler();
}
}
Create the #RabbitListner with parameters where rabbitErrorHandler is the bean that I defined previously :
#Override
#RabbitListener(queues = "${rabbit.queue}"
, errorHandler = "rabbitErrorHandler"
, returnExceptions = "true")
public ReturnObject receiveMessage(Message message) {
For the RabbitTemplate I set this attribute :
rabbitTemplate.setMessageConverter(new RemoteInvocationAwareMessageConverterAdapter());
When the messsage threated by the consumer, but it sent an error, I obtain a RemoteInvocationResult which contains the original exception into e.getCause().getCause().
See the returnExceptions property on #RabbitListener (since 2.0). Docs here.
The returnExceptions attribute, when true will cause exceptions to be returned to the sender. The exception is wrapped in a RemoteInvocationResult object.
On the sender side, there is an available RemoteInvocationAwareMessageConverterAdapter which, if configured into the RabbitTemplate, will re-throw the server-side exception, wrapped in an AmqpRemoteException. The stack trace of the server exception will be synthesized by merging the server and client stack traces.
Important
This mechanism will generally only work with the default SimpleMessageConverter, which uses Java serialization; exceptions are generally not "Jackson-friendly" so can’t be serialized to JSON. If you are using JSON, consider using an errorHandler to return some other Jackson-friendly Error object when an exception is thrown.
What worked for me was :
On "serving" side :
Service
#RabbitListener(id = "test1", containerFactory ="BEAN CONTAINER FACTORY",
queues = "TEST QUEUE", returnExceptions = "true")
DataList getData() {
// this exception will be transformed by rabbit error handler to a RemoteInvocationResult
throw new IllegalStateException("mon expecion");
//return dataHelper.loadAllData();
}
On "requesting" side :
Service
public void fetchData() throws AmqpRemoteException {
var response = (DataList) amqpTemplate.convertSendAndReceive("TEST EXCHANGE", "ROUTING NAME", new Object());
Optional.ofNullable(response)
.ifPresentOrElse(this::setDataContent, this::handleNoData);
}
Config
#Bean
AmqpTemplate amqpTemplate(ConnectionFactory connectionFactory, MessageConverter messageConverter) {
var rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setMessageConverter(messageConverter);
return rabbitTemplate;
}
#Bean
MessageConverter jsonMessageConverter() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
objectMapper.registerModule(new JavaTimeModule());
var jsonConverter = new Jackson2JsonMessageConverter(objectMapper);
DefaultClassMapper classMapper = new DefaultClassMapper();
Map<String, Class<?>> idClassMapping = Map.of(
DataList.class.getName(), DataList.class,
RemoteInvocationResult.class.getName(), RemoteInvocationResult.class
);
classMapper.setIdClassMapping(idClassMapping);
jsonConverter.setClassMapper(classMapper);
// json converter with returned exception awareness
// this will transform RemoteInvocationResult into a AmqpRemoteException
return new RemoteInvocationAwareMessageConverterAdapter(jsonConverter);
}
You have to return a message as an error, which the consuming application can choose to treat as an exception. However, I don't think normal exception handling flows apply with messaging. Your publishing application (the consumer of the RPC service) needs to know what can go wrong and be programmed to deal with those possibilities.

how to catch exception in spring boot rest api

i have a restcontroller with following Code
#RequestMapping(method = RequestMethod.POST, value = "/student")
public void addTopic(#RequestBody Student student) {
student.setPassword(bCryptPasswordEncoder.encode(student.getPassword()));
studentService.addStudent(student);
}
but if the json data doesn't match the Student object, or is wrong formatted an com.fasterxml.jackson.core.JsonParseException: Unexpected character ('"' (code 34)) ist thrown.
what is the best practice to prevent that
I've found that I need to catch JsonProcessingException (which JsonParseException extends from) in the #ExceptionHandler rather than JsonParseException
#ControllerAdvice
public class FeatureToggleControllerAdvice {
#ExceptionHandler(JsonProcessingException.class)
public ResponseEntity<JSONAPIDocument> handleJsonParseException(JsonProcessingException ex) {
final Error error = new Error();
error.setId(UUID.randomUUID().toString());
error.setStatus(HttpStatus.BAD_REQUEST.toString());
error.setTitle(ex.getMessage());
return new ResponseEntity<>(JSONAPIDocument
.createErrorDocument(Collections.singleton(error)), HttpStatus.NOT_FOUND);
}
}
Using JsonParseException in the above sample and nothing is caught, but using JsonProcessingException works as expected.
Use Spring ExceptionHandler to do that
You could specify an ExceptionHandler based on Exception types and also apply the error codes you want to use.
#ExceptionHandler(JsonParseException.class)
public JacksonExceptionHandler {
public ResponseEntity<String> handleError(final Exception exception) {
HttpStatus status = HttpStatus.BAD_REQUEST;
if (exception != null) {
LOGGER.warn("Responding with status code {} and exception message {}", status, exception.getMessage());
return new ResponseEntity<>(exception.getMessage(), status);
}
}
Furthermore you could make use of javax.validation to validate the entity you receive and then Spring Boot will do all the validation automagically. Just add #Valid to the body.

Spring boot rest mvc json parser is not validating input json data which is illegal or has trailing data

In my rest client i am passing the below JSON request data:
{
"jobName":"test1",
"source":{ "name":"prod1","type":"teradata"},
"target":{ "name":"prod2","type":"teradata"},
"objects":{ "name":"table1"}<br/>
}
junkdata ; ##%$##%
So the extra "junkdata ; ##%$##%" not got validated by the rest client or by the spring jackson out-of-the box message converter.
I did debug the code, the spring HttpServletRequest body has the complete data including the junk data. As such its not failing, the spring is ignoring the junk data and converting the starting JSON data into Java object.
I did try by adding annotations like #JsonFormat for #RequestBody in rest controller calls (#RestController). But its not validating seems the Spring out-of-the box message converter jackson is not validating the incoming request JSON data properly.
Now this issue (failing on trailing tokens or data) is fixed in the spring jackson https://github.com/FasterXML/jackson-databind/issues/1583
using DeserializationFeature.FAIL_ON_TRAILING_TOKENS, Here is the code sample to fix the issue:
#Configuration
public class RestControllerConfiguration extends WebMvcConfigurationSupport
{
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.FAIL_ON_TRAILING_TOKENS, true);
converters.add(new MappingJackson2HttpMessageConverter(objectMapper));
}
}
After working on different ways i got the solution using Google gson.jar, #Pete yes i have validate the JSON input which is invalid.
The google gson api is validating it properly, we need to use the custom message converter to validate it in the rest WebMvcConfigurationSupport class.
#Configuration
#ComponentScan(basePackages = { "com.teradata.datamovement.rest.controllers",
"com.teradata.rest.controller" })
public class RestControllerConfiguration extends WebMvcConfigurationSupport
{
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
log.debug("Adding custom message converter.");
converters.add(new AbstractHttpMessageConverter<Object>(MediaType.APPLICATION_JSON, new MediaType("application", "*+json")){
#Override
protected Object readInternal(Class<? extends Object> clazz,
HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
try{
log.debug("Converting and validating the http request body data.");
String httpRequestBody = convertStreamToString(inputMessage.getBody());
log.debug("Http request body data:"+httpRequestBody);
return new Gson().fromJson(httpRequestBody, clazz);
}
catch(JsonSyntaxException e){
throw new HttpMessageNotReadableException("Invalid input JSON data: " + e.getMessage(), e);
}
}
private String convertStreamToString(InputStream is) throws IOException {
if (is != null) {
Writer writer = new StringWriter();
char[] buffer = new char[1024];
try {
Reader reader = new BufferedReader(new InputStreamReader(is, "UTF-8"));
int n;
while ((n = reader.read(buffer)) != -1) {
writer.write(buffer, 0, n);
}
} finally {
is.close();
}
return writer.toString();
} else {
return "";
}
}
#Override
protected boolean supports(Class clazz) {
return true;
}
#Override
protected void writeInternal(Object t, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
outputMessage.getBody().write(new Gson().toJson(t).getBytes());
}
});
}
}
But the weird thing i have noticed is that its working only if i make it as anonymous class or by adding the class with in the same file. If i create this custom message converter out side this RestControllerConfiguration.java file, then its not validating it.
Here is the example:
{
"jobName":"test1",
"source":{ "name":"prod1","type":"teradata"},
"target":{ "name":"prod2","type":"teradata"},
"objects":{ "name":"table1"}
}
junkdata ; ##%$##%
This will get validated, and will throw error like
{"message":"Invalid input JSON data: com.google.gson.stream.MalformedJsonException: Expected EOF at line 7 column 1; nested exception is com.google.gson.JsonSyntaxException: com.google.gson.stream.MalformedJsonException: Expected EOF at line 7 column 1"}

Getting and using remote JSON data

I'm working on a little app and using GWT to build it.
I just tried making a request to a remote server which will return a response as JSON.
I've tried using the overlay types concept but I couldn't get it working. I've been changing the code around so its a bit off from where the Google GWT tutorials left.
JavaScriptObject json;
public JavaScriptObject executeQuery(String query) {
String url = "http://api.domain.com?client_id=xxxx&query=";
RequestBuilder builder = new RequestBuilder(RequestBuilder.GET,
URL.encode(url + query));
try {
#SuppressWarnings("unused")
Request request = builder.sendRequest(null, new RequestCallback() {
public void onError(Request request, Throwable exception) {
// violation, etc.)
}
public void onResponseReceived(Request request,
Response response) {
if (200 == response.getStatusCode()) {
// Process the response in response.getText()
json =parseJson(response.getText());
} else {
}
}
});
} catch (RequestException e) {
// Couldn't connect to server
}
return json;
}
public static native JavaScriptObject parseJson(String jsonStr) /*-{
return eval(jsonStr );
;
}-*/;
In the chrome's debugger I get umbrellaexception, unable to see the stack trace and GWT debugger dies with NoSuchMethodError... Any ideas, pointers?
You may have a look to GWT AutoBean framework.
AutoBean allow you to serialize and deserialize JSON string from and to Plain Old Java Object.
For me this framework became essential :
Code is cleaner than with JSNI objects (JavaScript Native Interface)
No dependancy with Framework not supported by Google (like RestyGWT)
You just define interfaces with getters and setters :
// Declare any bean-like interface with matching getters and setters,
// no base type is necessary
interface Person {
Address getAddress();
String getName();
void setName(String name):
void setAddress(Address a);
}
interface Address {
String getZipcode();
void setZipcode(String zipCode);
}
Later you can serialize or deserialize JSON String using a factory (See documentation) :
// (...)
String serializeToJson(Person person) {
// Retrieve the AutoBean controller
AutoBean<Person> bean = AutoBeanUtils.getAutoBean(person);
return AutoBeanCodex.encode(bean).getPayload();
}
Person deserializeFromJson(String json) {
AutoBean<Person> bean = AutoBeanCodex.decode(myFactory, Person.class, json);
return bean.as();
}
// (...)
First post on Stack Overflow (!) : I hope this help :)
Use JsonUtils#safeEval() to evaluate the JSON string instead of calling eval() directly.
More importantly, don't try to pass the result of an asynchronous call (like RequestBuilder#sendRequest() back to a caller using return - use a callback:
public void executeQuery(String query,
final AsyncCallback<JavaScriptObject> callback)
{
...
try {
builder.sendRequest(null, new RequestCallback() {
public void onError(Request request, Throwable caught) {
callback.onFailure(caught);
}
public void onResponseReceived(Request request, Response response) {
if (Response.SC_OK == response.getStatusCode()) {
try {
callback.onSuccess(JsonUtils.safeEval(response.getText()));
} catch (IllegalArgumentException iax) {
callback.onFailure(iax);
}
} else {
// Better to use a typed exception here to indicate the specific
// cause of the failure.
callback.onFailure(new Exception("Bad return code."));
}
}
});
} catch (RequestException e) {
callback.onFailure(e);
}
}
Generally, the workflow you're describing consists of four steps:
Make the request
Receive the JSON text
Parse the JSON in JavaScript objects
Describe these JavaScript objects using an overlay type
It sounds like you've already got steps 1 and 2 working properly.
Parse the JSON
JSONParser.parseStrict will do nicely. You'll be returned a JSONValue object.
This will allow you to avoid using your custom native method and will also make sure that it prevents arbitrary code execution while parsing the JSON. If your JSON payload is trusted and you want raw speed, use JSONParser.parseLenient. In either case, you need not write your own parser method.
Let's say that you're expecting the following JSON:
{
"name": "Bob Jones",
"occupations": [
"Igloo renovations contractor",
"Cesium clock cleaner"
]
}
Since you know that the JSON describes an object, you can tell the JSONValue that you're expecting to get a JavaScriptObject.
String jsonText = makeRequestAndGetJsonText(); // assume you've already made request
JSONValue jsonValue = JSONParser.parseStrict(jsonText);
JSONObject jsonObject = jsonValue.isObject(); // assert that this is an object
if (jsonObject == null) {
// uh oh, it wasn't an object after
// do error handling here
throw new RuntimeException("JSON payload did not describe an object");
}
Describe as an overlay type
Now that you know that your JSON describes an object, you can get that object and describe it in terms of a JavaScript class. Say you have this overlay type:
class Person {
String getName() /*-{
return this.name;
}-*/;
JsArray getOccupations() /*-{
return this.occupations;
}-*/;
}
You can make your new JavaScript object conform to this Java class by doing a cast:
Person person = jsonObject.getJavaScriptObject().cast();
String name = person.getName(); // name is "Bob Jones"
Using eval is generally dangerous, and can result in all kinds of strange behavior, if the server returns invalid JSON (note, that it's necessary, that the JSON top element is an array, if you simply use eval(jsonStr)!). So I'd make the server return a very simple result like
[ "hello" ]
and see, if the error still occurs, or if you can get a better stack trace.
Note: I assume, that the server is reachable under the same URL + port + protocol as your GWT host page (otherwise, RequestBuilder wouldn't work anyway due to Same Origin Policy.)
You actually don't need to parse the JSON, you can use native JSNI objects (JavaScript Native Interface).
Here's an example I pulled from a recent project doing basically the same thing you're doing:
public class Person extends JavaScriptObject{
// Overlay types always have protected, zero argument constructors.
protected Person(){}
// JSNI methods to get stock data
public final native String getName() /*-{ return this.name; }-*/;
public final native String getOccupation() /*-{ return this.occupation; }-*/;
// Non-JSNI methods below
}
and then to retrieve it like so:
/**
* Convert the string of JSON into JavaScript object.
*
*/
private final native JsArray<Person> asArrayOfPollData(String json) /*-{
return eval(json);
}-*/;
private void retrievePeopleList(){
errorMsgLabel.setVisible(false);
String url = JSON_URL;
url = URL.encode(url);
RequestBuilder builder = new RequestBuilder(RequestBuilder.POST, url);
try{
#SuppressWarnings("unused")
Request request = builder.sendRequest(null, new RequestCallback() {
#Override
public void onResponseReceived(Request req, Response resp) {
if(resp.getStatusCode() == 200){
JsArray<Person> jsonPeople = asArrayOfPeopleData(resp.getText());
populatePeopleTable(people);
}
else{
displayError("Couldn't retrieve JSON (" + resp.getStatusText() + ")");
}
}
#Override
public void onError(Request req, Throwable arg1) {
System.out.println("couldn't retrieve JSON");
displayError("Couldn't retrieve JSON");
}
});
} catch(RequestException e) {
System.out.println("couldn't retrieve JSON");
displayError("Couldn't retrieve JSON");
}
}
So essentially you're casting the response as an array of JSON Objects. Good stuff.
More info here: http://code.google.com/webtoolkit/doc/latest/DevGuideCodingBasicsJSNI.html