8

I am new to ReactiveX for Java and I've the following code block that make external http call but it is not async. We are using rxjava 1.2, and Java 1.8

  private ResponseEntity<String> callExternalUrl(String url, String json, HttpMethod method) {

    RestTemplate restTemplate;
    HttpEntity request;

      request = new HttpEntity(jsonContent, httpHeaders);

    return restTemplate.exchange(url, httpMethod, request, String.class);

  }

I've the following code block I found online but I couldn't totally understand it and how I can apply it to my code base.

private RxClient<RxObservableInvoker> httpClient;
public <T> Observable<T> fetchResult(String url, Func1<Response, T> mapper) {

    return httpClient.target(url)
        .request()
        .rx()
        .get()
        .subscribeOn(Schedulers.io())
        .map(mapper);
  }
WowBow
  • 7,137
  • 17
  • 65
  • 103

1 Answers1

8

If I understand you correctly, you need something like this to wrap your existing callExternalUrl

static Observable<String> callExternalUrlAsync(String url, String json, HttpMethod method)
{
    return Observable.fromCallable(() -> callExternalUrl(url, json, method))
            .subscribeOn(Schedulers.io())
            .flatMap(re -> {
                         if (re.hasBody())
                             return Observable.just(re.getBody());
                         else
                             return Observable.error(new RuntimeException("Bad response status " + re.getStatusCode()));
                     },
                     e -> Observable.error(e),
                     (Func0<Observable<? extends String>>) (() -> Observable.empty())) // I need explicit cast or it won't compile :-(
            .observeOn(Schedulers.computation());
}

Short description of the code:

  1. It schedules execution of the existing callExternalUrl on the Schedulers.io
  2. Does minimal transformation of ResponseEntity<T> into successful T and error cases. It happens on the io scheduler as well but it is not important as it is really short. (If there was an exception inside callExternalUrl, it is passed as is.)
  3. Makes subscriber to the result to be executed on Schedulers.computation

Caveats:

  1. You probably want to use your custom schedulers for both subscribeOn and observeOn
  2. You probably want to have some better logic in the first lambda passed to flatMap to distinguish between success and error and definitely you want some more specific exception type.

Higher-order magic

If you are willing to use higher-order functions and trade a little bit of performance for less code duplication you can do something like this:

// Universal wrapper method
static <T> Observable<T> wrapCallExternalAsAsync(Func3<String, String, HttpMethod, ResponseEntity<T>> externalCall, String url, String json, HttpMethod method)
{
    return Observable.fromCallable(() -> externalCall.call(url, json, method))
            .subscribeOn(Schedulers.io())
            .flatMap(re -> {
                         if (re.hasBody())
                             return Observable.just(re.getBody());
                         else
                             return Observable.error(new RuntimeException("Bad response status " + re.getStatusCode()));
                     },
                     e -> Observable.error(e),
                     (Func0<Observable<? extends T>>) (() -> Observable.empty())) // I need explicit cast or it won't compile :-(
            .observeOn(Schedulers.computation());
}

static Observable<String> callExternalUrlAsync_HigherOrder(String url, String json, HttpMethod method)
{
    return wrapCallExternalAsAsync(MyClass::callExternalUrl, url, json, method);
}

Where MyClass is wherever your callExternalUrl is.


Update (Async calls only)

private static RxClient httpClient = Rx.newClient(RxObservableInvoker.class); // here you might pass custom ExecutorService

private <T> Observable<String> executeHttpAsync(String url, String httpMethod, Entity<T> entity) {
    return httpClient.target(url)
            .request()
            .headers(httpHeaders) // assuming httpHeaders is something global as in your example
            .rx()
            .method(httpMethod, entity)
            .map(resp -> {
                if (200 != resp.getStatus()) {
                    throw new RuntimeException("Bad status code " + resp.getStatus());
                } else {
                    if (!resp.hasEntity()) {
                        // return null; // or error?
                        throw new RuntimeException("Empty response"); // or empty?
                    } else {
                        try {
                            return resp.readEntity(String.class);
                        } catch (Exception ex) {
                            throw new RuntimeException(ex); // wrap exception into unchecked
                        }
                    }
                }
            })
            .observeOn(Schedulers.computation());
}

private Observable<String> executeGetAsync(String url) {
    return executeHttpAsync(url, "GET", null);
}

private Observable<String> executePostAsync(String url, String json) {
    return executeHttpAsync(url, "POST", Entity.json(json));
}

Again similar caveats apply:

  1. You probably want to use your custom schedulers for both newClient call and observeOn
  2. You probably want to have some better logic for error handling than just checking whether it is HTTP 200 or not and definitely you want some more specific exception type. But this is all business-logic specific so it is up to you.

Also it is not clear from your example how exactly the body of the request (HttpEntity) is build and whether you actually always want String as a response as it is in your original example. Still I just replicated your logic as is. If you need something more you probably should refer to the documentation at https://jersey.java.net/documentation/2.25/media.html#json

SergGr
  • 23,570
  • 2
  • 30
  • 51
  • Thanks for your response. I could try what you did but I forgot to include that we need to use RxClient httpClient; – WowBow Mar 07 '17 at 20:08
  • When you said "I need explicit cast or it won't compile" which part are you trying to cast and to what type ? Thanks – WowBow Mar 07 '17 at 20:11
  • @WowBow, the last parameter of `flatMap` is expected to be of type `Func0 extends Observable extends T>>` but by on my machine lambda expression `() -> Observable.empty()` is inferred with some more specific functional type and doesn't compiled. As for "we need to use RxClient", do you mean you don't need your original synchronous methods and are OK with getting only async ones? – SergGr Mar 07 '17 at 20:29
  • Yes, I have to get rid of rest template and use RxClient async calls. I appericiate if you can add how to implement it that way (including headers). – WowBow Mar 07 '17 at 20:34
  • @WowBow, see my update at `Added async only solution` – SergGr Mar 07 '17 at 22:57
  • Thank you again. I'll try it out and let you know. As you see from my code httpEntity is populated as follows: HttpEntity request= new HttpEntity(jsonContent, httpHeaders); the caller method should provide the json content and headers. Regarding response restTemplate.exchange(..) returns ResponseEntity and I have to go through the string to fetch what I need. That is why I wanted the response back as a ResponseEntity. If there is a better way, I'm welcome to try it out. – WowBow Mar 07 '17 at 23:05
  • I couldn't get this working. What is and Entity representing in private Observable executeHttpAsync(String url, String httpMethod, Entity entity) ?? – WowBow Mar 13 '17 at 17:24
  • @WowBow, `Entity` here is supposed to be `javax.ws.rs.client.Entity` (https://jersey.java.net/apidocs/2.25.1/jersey/javax/ws/rs/client/Entity.html) and the call `.method(httpMethod, entity)` is https://jersey.java.net/apidocs/2.25.1/jersey/org/glassfish/jersey/client/rx/spi/AbstractRxInvoker.html#method(java.lang.String,%20javax.ws.rs.client.Entity) Could you clarify what doesn't work for you? Does it just not compile or is there some behavioral issue? – SergGr Mar 13 '17 at 20:26
  • @SerGr Not sure the use of entity here. And I am not sure what to pass as an "Entity" .. Same thing goes with "T". What is the purpose of making it generic here ? May be can you show me how you would call this method and use the result in the caller method. Assuming that all the rest of variables have some data already except Entity ? Thanks – WowBow Mar 13 '17 at 20:34
  • @WowBow, have you seen `executeGetAsync` and `executePostAsync` methods nearby? I think they provide some examples on how to pass data that are matching your original `executePostAsync` example. Actually I would expect that you should use those two "wrapper" methods instead of `executeHttpAsync` most of the time. You may also need to create some other wrappers for other cases but those two seem to match your original example. – SergGr Mar 13 '17 at 20:36
  • My bad. I was only focusing on the executeHttpAsync. I was zoned :P .. Will update my code and execute. Thanks. – WowBow Mar 13 '17 at 20:39
  • Hey brother. I am still confused with this. The url is returning a string (json that I need to extract) but your methods return Obersrvable. How do I make executeGetAsync return string or any other callers convert this observable data to string? I am totally lost. – WowBow Mar 15 '17 at 21:52
  • @WowBow, You can't simply convert `Obersrvable` to `String`. That's one of main ideas behind asynchronous processing. You should use something like `map`, `flatMap` or `subscribe`. Now seeing your question I'm not sure what was your goal of migrating those methods to RxJava – SergGr Mar 16 '17 at 18:59
  • thanks again man. It took me a lot of reading to understand that part. Now code works fine and its mapped to String.class which returns me the json string. which contains something like { "@odata.context": "url)/$entity", "id": "AAMkADI0ODFkMAAuAAAAAAADA8NqondtRrnNnWWj-4bNAQDGrQj3k2EVTJ4zVor0GaYiAAAAAAEMAAA=", "age": 42 } ... If i wanted to read only age and id from this data, how do I map it to a new class instead of just String (to make it more elegant but your answer is already good). – WowBow Mar 23 '17 at 15:27
  • @WowBow, have you read a link I mentioned https://jersey.java.net/documentation/2.25/media.html#json ? There is also ['Rx.from' method](https://jersey.java.net/apidocs/2.16/jersey/org/glassfish/jersey/client/rx/Rx.html#from\(javax.ws.rs.client.Client,%20java.lang.Class\)) that allows you to build an `RxClient` from a `Client` configured to auto-parse JSON as in the first link. (From that list I once worked with Jackson and it seemed to work fine but I expect that all the ways described there should work OK) – SergGr Mar 23 '17 at 15:52
  • Actually I had a method which returns the data I need. All I had to do was create a mapper and make that method return observable Observable. – WowBow Mar 23 '17 at 21:32
  • Now last question, I wanted to send json object in the request. I used request = new HttpEntity(jsonContent, httpHeaders); with HttpEntity but couldnt figure out how to add that in the method you provided here ? Thanks. – WowBow Mar 23 '17 at 21:34
  • @WowBow, Plan about "make that method return observable Observable" seems suspicious to me but if it works for you - OK. As for your last question, I don't understand where exactly you see problem. In `executeHttpAsync` there is a line `.headers(httpHeaders) ` and comment that from you code it looks like headers are some static var but of course you can pass it explicitly. If you trouble is that you want to put some object instead of `String`, then `Entity.json` method is actually generic and you can do it. – SergGr Mar 23 '17 at 22:17
  • I am just a bit confused. The content I had " request = new HttpEntity(jsonContent, httpHeaders);" is going with request body (hence jsonContent inside request not header.) Are you suggesting I should put the jsonContent in the header? I put it in the request body and I am receiving "The request must be chunked or have a content length". – WowBow Mar 24 '17 at 17:03
  • 1
    @WowBow, so now you have a different issue that when you send post requests you get HTTP 411 error "The request must be chunked or have a content length"? I don't know why it happens, in my local tests I can see valid `Content-Length` sent as a part of the POST request. I think you need to show your actual current code and probably even create a new question as it gets too complicated for comments. It is very hard to guess without a [Minimal, Complete, and Verifiable example](https://stackoverflow.com/help/mcve) – SergGr Mar 24 '17 at 17:26
  • I posted my new question here. Please help. http://stackoverflow.com/questions/43242926/how-to-add-a-parameter-to-rxclient-web-request – WowBow Apr 06 '17 at 14:24