aboutsummaryrefslogtreecommitdiffstats
path: root/src/eu/equalparts/cardbase/utils/MTGUniverse.java
blob: 98067fab4b198ee56c015deff0e711ce890c4693 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
package eu.equalparts.cardbase.utils;

import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;

import eu.equalparts.cardbase.cards.Card;
import eu.equalparts.cardbase.cards.CardSetInformation;
import eu.equalparts.cardbase.cards.FullCardSet;

/**
 * Access point to the complete set of cards that exist in the
 * MTG universe. This class has a series of methods that
 * query remote databases to acquire card information.
 * <br>
 * Conversely, {@code Cardbase}'s methods are used solely to
 * acquire information regarding the loaded cardbase, which will
 * most likely contain only a subset of the MTG universe of cards.
 * 
 * @author Eduardo Pedroni
 */
public final class MTGUniverse {

	/**
	 * The base URL from where the information is fetched.
	 */
	private final String BASE_DATA_URL = "http://mtgjson.com/json/";
	/**
	 * If the upstream set code list can't be loaded, this is loaded instead.
	 */
	private final String FALLBACK_LIST_PATH = "/setlist.json";
	/**
	 * A cache of CardSets to avoid querying the server many times for the same information.
	 */
	private List<CardSetInformation> cardSets;
	/**
	 * A cache of {@code FullCardSets} to avoid querying the server many times for the same information.
	 */
	private HashMap<String, FullCardSet> cardSetCache = new HashMap<String, FullCardSet>();
	
	/**
	 * Returns the specified card in the form of a {@code Card} object. If the specified number does
	 * not correspond to a card in the set or the specified set code does not correspond to a set,
	 * this returns null.
	 * 
	 * @param setCode the set to which the requested card belongs.
	 * @param number the requested card's set number.
	 * @return the requested {@code Card} or null if no card is found.
	 * 
	 * @throws JsonParseException if the upstream JSON is not formatted correctly.
	 * @throws JsonMappingException if the upstream JSON does not map to a local class.
	 * @throws IOException if a low-level I/O problem (unexpected end-of-input, network error) occurs.
	 */
	public Card getCard(String setCode, String number) throws JsonParseException, JsonMappingException, IOException {
		Card card = null;
		FullCardSet fullCardSet = getFullCardSet(setCode);
		
		if (fullCardSet != null) {
			card = fullCardSet.getCardByNumber(number);
		}
		
		return card;
	}
	
	/**
	 * Returns the specified set in the form of a {@code FullCardSet} object. If the specified
	 * set code does not correspond to a set, this returns null.
	 * <br>
	 * This method takes care of case differences in set code names.
	 * 
	 * 
	 * @param setCode the code of the set to be returned.
	 * @return the requested {@code FullCardSet} or null if no set matches the given code.
	 * 
	 * @throws JsonParseException if the upstream JSON is not formatted correctly.
	 * @throws JsonMappingException if the upstream JSON does not map to {@code FullCardSet}.
	 * @throws IOException if a low-level I/O problem (unexpected end-of-input, network error) occurs.
	 */
	public FullCardSet getFullCardSet(String setCode) throws JsonParseException, JsonMappingException, IOException {
		FullCardSet requestedSet = null;
		String validCode = validateSetCode(setCode);
		if (validCode != null) {
			// if the set is cached, no need to fetch
			if (cardSetCache.containsKey(validCode)) {
				requestedSet = cardSetCache.get(validCode);
			} 
			// not cached; fetch and cache
			else {
				requestedSet = parseFullSet(JSON.mapper.readValue(new URL(BASE_DATA_URL + validCode + ".json"), JsonNode.class));
				cardSetCache.put(validCode, requestedSet);
			}
		}
		return requestedSet;
	}
	
	/**
	 * @return a list of all card sets in the form of {@code CardSet} objects.
	 */
	public List<CardSetInformation> getCardSetList() {
		// if the list isn't cached, fetch and cache it
		if (cardSets == null) {
			try {
				cardSets = JSON.mapper.readValue(new URL(BASE_DATA_URL + "SetList.json"), new TypeReference<ArrayList<CardSetInformation>>() {});
			} catch (Exception e) {
				System.out.println("Error: could not fetch or parse set code list from upstream, using fallback json...");
				e.printStackTrace();
				
				try {
					cardSets = JSON.mapper.readValue(MTGUniverse.class.getResourceAsStream(FALLBACK_LIST_PATH), new TypeReference<ArrayList<CardSetInformation>>() {});
				} catch (Exception f) {
					System.out.println("Error: could not parse fallback set code list, aborting...");
					f.printStackTrace();
					System.exit(1);
				}
			}
		}
		return cardSets;
	}
	
	/**
	 * This method effectively converts different set code spellings
	 * into the format parsed from the set code list.
	 * <br>
	 * For instance, if "m15" is passed as the argument, this returns
	 * "M15", which is the set code as it is stated in the formal list.
	 * If the specified set code does not approximate any entry from
	 * the list, this returns null.
	 * 
	 * @param setCode the set code to be validated.
	 * @return the valid form of the set code if any, null otherwise.
	 */
	public String validateSetCode(String setCode) {
		for (CardSetInformation cardSet : getCardSetList()) {
			if (cardSet.getCode().equalsIgnoreCase(setCode)) {
				return cardSet.getCode();
			}
		}
		return null;
	}
	
	/**
	 * This method is necessary to adapt the list of cards in the json to
	 * the map format used in cardbase.
	 * 
	 * @param jsonTree the tree-representation of the json to be parsed.
	 * @return the parsed full card set.
	 * 
	 * @throws JsonMappingException if the upstream JSON does not map to {@code FullCardSet}.
	 * @throws IOException if a low-level I/O problem (unexpected end-of-input, network error) occurs.
	 */
	private FullCardSet parseFullSet(JsonNode jsonTree) throws JsonMappingException, IOException {
		
		FullCardSet fcs = new FullCardSet();
		
		/*
		 * These fields are critical, if any of them is not present an exception is thrown.
		 */
		if (jsonTree.hasNonNull("name")) {
			fcs.name = jsonTree.get("name").asText();
		} else {
			throw new JsonMappingException("Field \"name\" not found.");
		}
		
		String setCode;
		if (jsonTree.hasNonNull("code")) {
			setCode = jsonTree.get("code").asText();
			fcs.code = setCode;
		} else {
			throw new JsonMappingException("Field \"code\" not found.");
		}
		
		String imageCode;
		if (jsonTree.hasNonNull("magicCardsInfoCode")) {
			imageCode = jsonTree.get("magicCardsInfoCode").asText();
			fcs.magicCardsInfoCode = imageCode;
		} else {
			throw new JsonMappingException("Field \"magicCardsInfoCode\" not found.");
		}
		
		if (jsonTree.hasNonNull("releaseDate")) {
			fcs.releaseDate = jsonTree.get("releaseDate").asText();
		} else {
			throw new JsonMappingException("Field \"releaseDate\" not found.");
		}
		
		if (jsonTree.hasNonNull("cards")) {
			// attempt to read card list as a POJO using the standard mapper
			List<Card> rawList = jsonTree.get("cards").traverse(JSON.mapper).readValueAs(new TypeReference<List<Card>>() {});
			// generate the map
			Map<String, Card> cardMap = new HashMap<String, Card>();
			for (Card card : rawList) {
				// add set code for convenience
				card.setCode = setCode;
				card.imageCode = imageCode;
				cardMap.put(card.number, card);
			}
			fcs.cards = cardMap;
		} else {
			throw new JsonMappingException("Field \"cards\" not found.");
		}
		
		/*
		 * These fields are optional and are set to null if not present.
		 */
		fcs.gathererCode = jsonTree.hasNonNull("gathererCode") ? jsonTree.get("gathererCode").asText() : null;
		fcs.border = jsonTree.hasNonNull("border") ? jsonTree.get("border").asText() : null;
		fcs.type = jsonTree.hasNonNull("type") ? jsonTree.get("type").asText() : null;
		fcs.block = jsonTree.hasNonNull("block") ? jsonTree.get("block").asText() : null;
		
		return fcs;
	}
}