6

I'm trying to do something that is trivial in JavaScript, but seems complicated with Java. I'm hoping someone can point out how to do it simply in Java as well.

I want to call a REST JSON API, e.g. https://images-api.nasa.gov/search?q=clouds

I get back a data structure that, in a simplified form, looks something like this:

{
  "collection": {
    "items": [
      {
        "links": [
          {
            "href": "https://images-assets.nasa.gov/image/cloud-vortices_22531636120_o/cloud-vortices_22531636120_o~thumb.jpg",
            "rel": "preview"
          }
        ]
      }
    ]
  }
}

In Java, I want to call the URL and get the href Strings as a List.

In JavaScript, I would simply write

fetch("https://images-api.nasa.gov/search?q=moon")
  .then(data => data.json())
  .then(data => {
    const items = data
      .collection
      .items
      .map(item => item.links)
      .flat()
      .filter(link => link.rel === "preview")
      .map(link => link.href);

    // do something with "items"
})

1. My initial solution

With a little searching, I found this approach, which seems to be going in the right direction, but still very verbose.

String uri = "https://images-api.nasa.gov/search?q=clouds";
List<String> hrefs = new ArrayList<>();

try {
    // make the GET request
    URLConnection request = new URL(uri).openConnection();
    request.connect();
    InputStreamReader inputStreamReader = new InputStreamReader((InputStream) request.getContent());

    // map to GSON objects
    JsonElement root = new JsonParser().parse(inputStreamReader);

    // traverse the JSON data 
    JsonArray items = root
            .getAsJsonObject()
            .get("collection").getAsJsonObject()
            .get("items").getAsJsonArray();

    // flatten nested arrays
    JsonArray links = new JsonArray();
    items.forEach(item -> links.addAll(item
            .getAsJsonObject()
            .get("links")
            .getAsJsonArray()));

    // filter links with "href" properties
    links.forEach(link -> {
        JsonObject linkObject = link.getAsJsonObject();
        String relString = linkObject.get("rel").getAsString();
        if ("preview".equals(relString)) {
            hrefs.add(linkObject.get("href").getAsString());
        }
    });

} catch (IOException e) {
    e.printStackTrace();
}

return hrefs;

My remaining questions are:

  • Is there a way to use RestTemplate or some other library to make the GET Request less verbose and still keep the generic flexibility of GSON?
  • Is there a way to flatten nested JsonArrays and/or filter JsonArrays with GSON so I don't need to create additional temporary JsonArrays?
  • Are there any other ways to make the code less verbose ?

Edited

The following sections were added after reading the comments and answers below.


2. Less verbose solution

(as proposed in the answer by @diutsu)

List<String> hrefs = new ArrayList<>();
String json = new RestTemplate().getForObject("https://images-api.nasa.gov/search?q=clouds", String.class);
new JsonParser().parse(json).getAsJsonObject()
    .get("collection").getAsJsonObject()
    .get("items").getAsJsonArray()
    .forEach(item -> item.getAsJsonObject()
        .get("links").getAsJsonArray()
        .forEach(link -> {
            JsonObject linkObject = link.getAsJsonObject();
            String relString = linkObject.get("rel").getAsString();
            if ("preview".equals(relString)) {
                hrefs.add(linkObject.get("href").getAsString());
            }
        })
    );
return hrefs;

3. Solution using Mapper POJOs

(inspired by @JBNizet and @diutsu)

The actuall GET request and tranformation is now a one-liner and almost identical to the JavaScript code I posed above, ...

return new RestTemplate().getForObject("https://images-api.nasa.gov/search?q=clouds", CollectionWrapper.class)
    .getCollection()
    .getItems().stream()
    .map(Item::getLinks)
    .flatMap(List::stream)
    .filter(item -> "preview".equals(item.getRel()))
    .map(Link::getHref)
    .collect(Collectors.toList());

... but for this to work, I had to create the following 4 classes:

CollectionWrapper

public class CollectionWrapper {

    private Collection collection;

    public CollectionWrapper(){}

    public CollectionWrapper(Collection collection) {
        this.collection = collection;
    }

    public Collection getCollection() {
        return collection;
    }
}

Collection

public class Collection {

    private List<Item> items;

    public Collection(){}

    public Collection(List<Item> items) {
        this.items = items;
    }

    public List<Item> getItems() {
        return items;
    }
}

Item

public class Item {

    private List<Link> links;

    public Item(){}

    public Item(List<Link> links) {
        this.links = links;
    }

    public List<Link> getLinks() {
        return links;
    }
}

Link

public class Link {

    private String href;
    private String rel;

    public Link() {}

    public Link(String href, String rel) {
        this.href = href;
        this.rel = rel;
    }

    public String getHref() {
        return href;
    }

    public String getRel() {
        return rel;
    }
}

4. Using Kotlin

(inspired by @NBNizet)

val collectionWrapper = RestTemplate().getForObject("https://images-api.nasa.gov/search?q=clouds", CollectionWrapper::class.java);
return collectionWrapper
        ?.collection
        ?.items
        ?.map { item -> item.links }
        ?.flatten()
        ?.filter { item -> "preview".equals(item.rel) }
        ?.map { item -> item.href }
        .orEmpty()

Using Kotlin makes the mapper classes simpler, even simpler than using Java with Lombok

data class CollectionWrapper(val collection: Collection)
data class Collection(val items: List<Item>)
data class Item(val links: List<Link>)
data class Link(val rel: String, val href: String)

5. Mapping directly to Map and List

I'm not convinced this is a good idea, but good to know it can be done:

return 
    ( (Map<String, Map<String, List<Map<String, List<Map<String, String>>>>>>) 
        new RestTemplate()
        .getForObject("https://images-api.nasa.gov/search?q=clouds", Map.class)
    )
    .get("collection")
    .get("items").stream()
    .map(item -> item.get("links"))
    .flatMap(List::stream)
    .filter(link -> "preview".equals(link.get("rel")))
    .map(link -> link.get("href"))
    .collect(Collectors.toList());
4
  • 1
    Regardless of the HTTP client library, I generally prefer to let a JSON mapper (Jackson or Gson, typically) map the JSON to custom Java classes, using standard collections and typed and named properties. Commented Aug 1, 2019 at 19:42
  • @JBNizet I understand the idea, but it seems to me that's quite a lot of work to just access the data I needed in the example above, no? You couldn't even make the GET request before having created the whole model class structure, which in this case would mean creating at least the classes ItemsList, Item, LinkList and Link, each with their constructors and getters, annotations and toString(), hashCode() and equals(). Is that what you are referring to? Commented Aug 1, 2019 at 20:00
  • 1
    Yes. Java is not a dynamic language, so either you use JSONArray/JSONObject, or you use Maps and Lists, or you use actual Java objects. I use Kotlin, when I can, which makes creating those POJOS much simpler and less verbose. But that said, you don't need hashCode/equals/toString for such POJOs. And you don't need annotations either. Commented Aug 1, 2019 at 20:02
  • 2
    You don't need to define annotations, toString, hashCode and equals for every class. Also, have a look at lombook, it helps a little. Commented Aug 1, 2019 at 20:02

1 Answer 1

2

1) Get as a String

restTemplate.getForObject("https://images-api.nasa.gov/search?q=clouds", String.class)

2) Simple, don't use arrays. I would say its less readable, but you can extract some methods to help with that.

root.getAsJsonObject()
    .get("collection").getAsJsonObject()
    .get("items").getAsJsonArray()
    .forEach(item -> item.getAsJsonObject()
       .get("links").getAsJsonArray()
       .forEach(link -> {
            JsonObject linkObject = link.getAsJsonObject();
            String relString = linkObject.get("rel").getAsString();
            if ("preview".equals(relString)) {
               hrefs.add(linkObject.get("href").getAsString());
            }));

3) Not if you wan't to keep it simple :D You could define your own structure and then get to that structure directly from the restTemplate. It would be a one liner. but since you only care about the hrefs, it doesn't make sense.

Sign up to request clarification or add additional context in comments.

1 Comment

This is perfect. Thank you. I'll update my solution with this proposal.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.