15
\$\begingroup\$

I'm still working on a parser that can parse log entries from a game called HearthStone, the overall idea is that it will read the log file live when the game is running, parses the log file and show interesting and useful data in real time.

The focus for this question is to load the card data from JSON files, as can be found on http://hearthstonejson.com/, which represent accurate card data in HearthStone.

I wanted to not use any external libraries and I know Java 8 came with the new Nashorn ScriptEngine, so I decided to play around with that.

The main reason why I did not want to deal with external libraries is that in my opinion creating objects to store all properties of some item down a JSON object is really annoying and I want to quickly have a result available.

Of course it turned out to take somewhat longer than I hoped, but I am still happy with the result.


CardData.java

public class CardData {
    private static final CardDataLoader CARD_DATA_LOADER = new CardDataLoader();

    private final String id;
    private final String name;
    private final Optional<String> playerClass;

    public CardData(final String id, final String name, final Optional<String> playerClass) {
        this.id = Objects.requireNonNull(id, "id");
        this.name = Objects.requireNonNull(name, "name");
        this.playerClass = Objects.requireNonNull(playerClass, "playerClass");
    }

    public String getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public Optional<String> getPlayerClass() {
        return playerClass;
    }

    public static CardData getForCardId(final String cardId) {
        Objects.requireNonNull(cardId, "cardId");
        return CARD_DATA_LOADER.load(cardId);
    }

    private static class CardDataLoader {
        private final Map<String, CardData> cardDataMap = new HashMap<>();
        private final ScriptEngine scriptEngine;

        private CardDataLoader() {
            try {
                Path setListPath = Paths.get(CardData.class.getResource("SetList.json").toURI());
                String setList = new String(Files.readAllBytes(setListPath), StandardCharsets.UTF_8);

                Path allSetsPath = Paths.get(CardData.class.getResource("AllSets.json").toURI());
                String allSets = new String(Files.readAllBytes(allSetsPath), StandardCharsets.UTF_8);

                scriptEngine = new ScriptEngineManager().getEngineByName("nashorn");
                scriptEngine.eval(new InputStreamReader(new FileInputStream(Paths.get(CardData.class.getResource("CardData.js").toURI()).toFile()), StandardCharsets.UTF_8));

                Invocable invocable = (Invocable)scriptEngine;
                invocable.invokeFunction("setSetList", setList);
                invocable.invokeFunction("setAllSets", allSets);
            } catch (URISyntaxException | IOException | ScriptException | NoSuchMethodException ex) {
                throw new IllegalStateException("an error has occurred when initializing the card data loader", ex);
            }
        }

        private CardData load(final String cardId) {
            if (cardDataMap.containsKey(cardId)) {
                return cardDataMap.get(cardId);
            }
            try {
                CardData cardData = load0(cardId);
                cardDataMap.put(cardId, cardData);
                return cardData;
            } catch (ScriptException | NoSuchMethodException ex) {
                throw new IllegalStateException("an error occurred when loading cardId = " + cardId, ex);
            }
        }

        private CardData load0(final String cardId) throws ScriptException, NoSuchMethodException {
            Invocable invocable = (Invocable)scriptEngine;
            CardData cardData = (CardData)invocable.invokeFunction("getCardData", cardId);
            if (cardData == null) {
                throw new IllegalStateException("could not find card for cardId = " + cardId);
            }
            return cardData;
        }
    }
}

CardData.js

var CardData = Java.type("com.github.skiwi2.hearthmonitor.CardData");
var Optional = Java.type("java.util.Optional");
var setList;
var allSets;

function setSetList(setListRaw) {
    setList = JSON.parse(setListRaw);
}

function setAllSets(allSetsRaw) {
    allSets = JSON.parse(allSetsRaw);
}

function getCardData(cardId) {
    for (var i = 0; i < setList.length; i++) {
        var set = setList[i];
        for (var j = 0; j < allSets[set].length; j++) {
            var card = allSets[set][j];
            if (card.id == cardId) {
                return new CardData(card.id, card.name, Optional.ofNullable(card.playerClass));
            }
        }
    }
    return null;
}
\$\endgroup\$
12
\$\begingroup\$

You have an interesting approach here...

"I don't want to deal with external libraries because I don't want to create classes to store the properties. I know, I'll use JavaScript!"

Now you have two problems.

What you are doing

  • Load the entire approx 250kb file "AllSets.json" to a String (250kb memory, poof!)
  • Load the entire 119 byte file "SetList.json" to a String (119 byte memory, more acceptable)
  • Read the two strings you loaded into JSON objects, using Nashhorn
  • When looking up a card, you loop through all sets and all the cards in the set until you find a card with a matching id, then you return three properties on that card.

Your code

There's one major performance drawback in your code, and that's the getCardData function (JavaScript part).

You loop through all the sets and loop through all the cards in each set until you find what you are looking for.

Instead, loop through the cards once after you load them, and store the cards in an associative array (pretty much a pure old JavaScript object) so that you can quickly lookup the cards in your getCardData method with a lookup speed of \$O(1)\$.

How would I have done it?

Who said you have to create classes to store the properties? I believe most JSON libraries for Java comes with a method to read the JSON tree, without having to create classes to represent your data.

Just use Jackson's readTree method:

ObjectMapper mapper = new ObjectMapper();
JsonNode allSets = mapper.readTree(CardData.class.getResource("AllSets.json"));
JsonNode setList = mapper.readTree(CardData.class.getResource("SetList.json"));

Note that by using Jackson, and using the most raw data available (the URL), Jackson will not read it into a String first, and then convert to JSON, it will read it into JSON directly.

Now let's see what we can do with it, shall we?

Map<String, CardData> cardMap = new HashMap<>();
setList.forEach(node -> {
    JsonNode setNode = allSets.path(node.asText());
    setNode.forEach(card -> {
        CardData cardData = new CardData(card.get("id").asText(), card.get("name"), Optional.ofNullable(card.get("playerClass")));
        cardMap.put(cardData.getId(), cardData);
    });
});

Voilà. Now all you need is a method to retrieve the card data, very simple:

CardData getCardData(String id) {
    return cardMap.get(id);
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.