diff options
author | Marko Zajc <marko.zajc@protonmail.com> | 2020-08-02 19:19:41 +0200 |
---|---|---|
committer | Marko Zajc <marko.zajc@protonmail.com> | 2020-08-02 19:19:41 +0200 |
commit | 7618f7eb283074ac832d5cc5a1987d982f501c89 (patch) | |
tree | 58374339161c3eb07a12a4ac9e8fa2320bda957f | |
parent | e2fc0bbbdfc8785b4ee7245c8964dc7b33586599 (diff) | |
parent | 44c779e11731267a3afb158bd85c8e935424c3ae (diff) |
Merge branch 'development'v1.5
44 files changed, 1841 insertions, 1694 deletions
diff --git a/.settings/org.eclipse.wst.xsl.core.prefs b/.settings/org.eclipse.wst.xsl.core.prefs new file mode 100644 index 0000000..e28962c --- /dev/null +++ b/.settings/org.eclipse.wst.xsl.core.prefs | |||
@@ -0,0 +1,11 @@ | |||
1 | CHECK_CALL_TEMPLATES=2 | ||
2 | CHECK_XPATHS=2 | ||
3 | CIRCULAR_REF=2 | ||
4 | DUPLICATE_PARAMETER=2 | ||
5 | EMPTY_PARAM=1 | ||
6 | MISSING_INCLUDE=2 | ||
7 | MISSING_PARAM=1 | ||
8 | NAME_ATTRIBUTE_EMPTY=2 | ||
9 | NAME_ATTRIBUTE_MISSING=2 | ||
10 | TEMPLATE_CONFLICT=2 | ||
11 | eclipse.preferences.version=1 | ||
diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..cd54b01 --- /dev/null +++ b/.travis.yml | |||
@@ -0,0 +1,5 @@ | |||
1 | language: java | ||
2 | jdk: | ||
3 | - openjdk11 | ||
4 | sudo: false | ||
5 | script: mvn clean verify | ||
@@ -1,9 +1,15 @@ | |||
1 | [central]: https://img.shields.io/maven-central/v/com.github.markozajc/akiwrapper.svg?label=Maven%20Central | ||
2 | [travis]: https://travis-ci.org/markozajc/Akiwrapper.svg?branch=master | ||
3 | [![travis]](https://travis-ci.org/markozajc/Akiwrapper) | ||
4 | ![central] | ||
5 | |||
6 | |||
1 | # Akiwrapper | 7 | # Akiwrapper |
2 | Akiwrapper is a fully-documented and easy-to-use Java API wrapper for Akinator. | 8 | Akiwrapper is a fully-documented and easy-to-use Java API wrapper for Akinator. |
3 | 9 | ||
4 | ## Installation | 10 | ## Installation |
5 | #### Maven | 11 | #### Maven |
6 | Put this: into your pom.xml (replace LATEST_VERSION with [![Maven Central](https://img.shields.io/maven-central/v/com.github.markozajc/akiwrapper.svg?label=Maven%20Central)](https://search.maven.org/search?q=g:%22com.github.markozajc%22%20AND%20a:%22akiwrapper%22): | 12 | Put this: into your pom.xml (replace LATEST_VERSION with ![central]: |
7 | ```xml | 13 | ```xml |
8 | <dependency> | 14 | <dependency> |
9 | <groupId>com.github.markozajc</groupId> | 15 | <groupId>com.github.markozajc</groupId> |
diff --git a/example/src/main/java/com/markozajc/akiwrapper/example/AkinatorExample.java b/example/src/main/java/com/markozajc/akiwrapper/example/AkinatorExample.java index f16d4fd..3bfb3f4 100644 --- a/example/src/main/java/com/markozajc/akiwrapper/example/AkinatorExample.java +++ b/example/src/main/java/com/markozajc/akiwrapper/example/AkinatorExample.java | |||
@@ -1,19 +1,22 @@ | |||
1 | package com.markozajc.akiwrapper.example; | 1 | package com.markozajc.akiwrapper.example; |
2 | 2 | ||
3 | import java.util.ArrayList; | 3 | import java.util.ArrayList; |
4 | import java.util.EnumSet; | ||
4 | import java.util.List; | 5 | import java.util.List; |
5 | import java.util.Scanner; | 6 | import java.util.Scanner; |
6 | import java.util.stream.Collectors; | 7 | import java.util.stream.Collectors; |
7 | 8 | ||
9 | import javax.annotation.Nonnull; | ||
10 | |||
8 | import com.markozajc.akiwrapper.Akiwrapper; | 11 | import com.markozajc.akiwrapper.Akiwrapper; |
9 | import com.markozajc.akiwrapper.Akiwrapper.Answer; | 12 | import com.markozajc.akiwrapper.Akiwrapper.Answer; |
10 | import com.markozajc.akiwrapper.AkiwrapperBuilder; | 13 | import com.markozajc.akiwrapper.AkiwrapperBuilder; |
11 | import com.markozajc.akiwrapper.core.Route; | ||
12 | import com.markozajc.akiwrapper.core.entities.AkiwrapperMetadata; | ||
13 | import com.markozajc.akiwrapper.core.entities.Guess; | 14 | import com.markozajc.akiwrapper.core.entities.Guess; |
14 | import com.markozajc.akiwrapper.core.entities.Question; | 15 | import com.markozajc.akiwrapper.core.entities.Question; |
16 | import com.markozajc.akiwrapper.core.entities.Server.GuessType; | ||
15 | import com.markozajc.akiwrapper.core.entities.Server.Language; | 17 | import com.markozajc.akiwrapper.core.entities.Server.Language; |
16 | import com.markozajc.akiwrapper.core.utils.Servers; | 18 | import com.markozajc.akiwrapper.core.entities.impl.immutable.ApiKey; |
19 | import com.markozajc.akiwrapper.core.exceptions.ServerNotFoundException; | ||
17 | 20 | ||
18 | @SuppressWarnings("javadoc") | 21 | @SuppressWarnings("javadoc") |
19 | public class AkinatorExample { | 22 | public class AkinatorExample { |
@@ -21,7 +24,7 @@ public class AkinatorExample { | |||
21 | public static final double PROBABILITY_THRESHOLD = 0.85; | 24 | public static final double PROBABILITY_THRESHOLD = 0.85; |
22 | // This will be our probability threshold. | 25 | // This will be our probability threshold. |
23 | 26 | ||
24 | private static boolean reviewGuess(Guess guess, Scanner sc) { | 27 | private static boolean reviewGuess(@Nonnull Guess guess, @Nonnull Scanner sc) { |
25 | System.out.println(guess.getName()); | 28 | System.out.println(guess.getName()); |
26 | System.out.println("\t" + (guess.getDescription() == null ? "(no description)" : guess.getDescription())); | 29 | System.out.println("\t" + (guess.getDescription() == null ? "(no description)" : guess.getDescription())); |
27 | // Displays the guess. | 30 | // Displays the guess. |
@@ -66,80 +69,30 @@ public class AkinatorExample { | |||
66 | } | 69 | } |
67 | } | 70 | } |
68 | 71 | ||
72 | @SuppressWarnings("null") | ||
69 | public static void main(String[] args) throws Exception { | 73 | public static void main(String[] args) throws Exception { |
70 | try (Scanner sc = new Scanner(System.in)) { | 74 | try (Scanner sc = new Scanner(System.in)) { |
71 | 75 | Boolean filterProfanity = getProfanityFilter(sc); | |
72 | System.out.println("What's your name? (" + AkiwrapperMetadata.DEFAULT_NAME + ")"); | ||
73 | String name = sc.nextLine().trim(); | ||
74 | if (name.equals("")) | ||
75 | name = "desktopPlayer"; | ||
76 | // In case the user has just pressed <ENTER>, we'll use the default setting. | ||
77 | |||
78 | // Gets user's name (this won't be important in the future but is still done, | ||
79 | // for some reason). | ||
80 | |||
81 | Boolean filterProfanity = null; | ||
82 | { | ||
83 | System.out.println("What's your age? (18)"); | ||
84 | while (filterProfanity == null) { | ||
85 | String age = sc.nextLine(); | ||
86 | |||
87 | if (age.equals("")) { | ||
88 | filterProfanity = false; | ||
89 | continue; | ||
90 | } | ||
91 | |||
92 | try { | ||
93 | filterProfanity = Integer.parseInt(age) < 16; | ||
94 | // Tries to format the given number. | ||
95 | |||
96 | } catch (NumberFormatException e) { | ||
97 | System.out.println("That's not a real age!"); | ||
98 | // In case the given number is not formattable (too big or not a number). | ||
99 | } | ||
100 | } | ||
101 | } | ||
102 | // Gets user's age. Like the Akinator's website, this will turn on the profanity | 76 | // Gets user's age. Like the Akinator's website, this will turn on the profanity |
103 | // filter if the age entered is below 16. | 77 | // filter if the age entered is below 16. |
104 | 78 | ||
105 | Language localization = null; | 79 | Language language = getLanguage(sc); |
106 | { | ||
107 | List<Language> languages = new ArrayList<>(Servers.SERVER_GROUPS.keySet()); | ||
108 | // Fetches all available languages. | ||
109 | |||
110 | String unsupportedLanguageMessage = "Sorry, that language isn't supported. Rather try with:" | ||
111 | + languages.stream().map(Enum::toString).collect(Collectors.joining("\n-", "\n-", "")); | ||
112 | // Does some Java 8 magic to pre-prepare an error message. | ||
113 | |||
114 | System.out.println("What's your language? (English)"); | ||
115 | while (localization == null) { | ||
116 | String language = sc.nextLine().toLowerCase().trim(); | ||
117 | |||
118 | if (language.equals("")) { | ||
119 | localization = Language.ENGLISH; | ||
120 | continue; | ||
121 | } | ||
122 | |||
123 | Language matching = languages.stream() | ||
124 | .filter(l -> l.toString().toLowerCase().equals(language)) | ||
125 | .findAny() | ||
126 | .orElse(null); | ||
127 | |||
128 | if (matching == null) { | ||
129 | System.out.println(unsupportedLanguageMessage); | ||
130 | continue; | ||
131 | } | ||
132 | |||
133 | localization = matching; | ||
134 | } | ||
135 | } | ||
136 | // Gets user's language. Akinator will give the user localized questions and guesses | 80 | // Gets user's language. Akinator will give the user localized questions and guesses |
137 | // depending on user's language. | 81 | // depending on user's language. |
138 | 82 | ||
139 | Akiwrapper aw = new AkiwrapperBuilder().setName(name) | 83 | GuessType guessType = getGuessType(sc); |
140 | .setFilterProfanity(filterProfanity) | 84 | // Gets the guess type. |
141 | .setLocalization(localization) | 85 | |
142 | .build(); | 86 | Akiwrapper aw; |
87 | try { | ||
88 | aw = new AkiwrapperBuilder().setFilterProfanity(filterProfanity) | ||
89 | .setLanguage(language) | ||
90 | .setGuessType(guessType) | ||
91 | .build(); | ||
92 | } catch (ServerNotFoundException e) { | ||
93 | System.err.println("Invalid combination of language and guess type. Try a different guess type."); | ||
94 | return; | ||
95 | } | ||
143 | // Builds the Akiwrapper instance, this is what we'll be using to perform | 96 | // Builds the Akiwrapper instance, this is what we'll be using to perform |
144 | // operations such as answering questions, fetching guesses, etc. | 97 | // operations such as answering questions, fetching guesses, etc. |
145 | 98 | ||
@@ -161,83 +114,182 @@ public class AkinatorExample { | |||
161 | 114 | ||
162 | if (question.getStep() == 0) | 115 | if (question.getStep() == 0) |
163 | System.out.println( | 116 | System.out.println( |
164 | "\nAnswer with Y (yes), N (no), DK (don't know), P (probably) or PN (probably not) or go back in time with B (back)."); | 117 | "\nAnswer with Y (yes), N (no), DK (don't know), P (probably) or PN (probably not) or go back in time with B (back)."); |
165 | // Displays the tip (only for the first time). | 118 | // Displays the tip (only for the first time). |
166 | 119 | ||
167 | boolean answered = false; | 120 | answerQuestion(sc, aw); |
168 | while (!answered) { | ||
169 | // Iterates while the questions remains unanswered. | ||
170 | 121 | ||
171 | String answer = sc.nextLine().toLowerCase(); | 122 | reviewGuesses(sc, aw, declined); |
123 | // Iterates over any available guesses. | ||
124 | } | ||
172 | 125 | ||
173 | if (answer.equals("y")) { | 126 | for (Guess guess : aw.getGuesses()) { |
174 | aw.answerCurrentQuestion(Answer.YES); | 127 | if (reviewGuess(guess, sc)) { |
128 | // Reviews all final guesses. | ||
129 | finish(true); | ||
130 | System.exit(0); | ||
131 | } | ||
132 | } | ||
175 | 133 | ||
176 | } else if (answer.equals("n")) { | 134 | finish(false); |
177 | aw.answerCurrentQuestion(Answer.NO); | 135 | // Loses if all guesses are rejected. |
136 | } | ||
137 | } | ||
178 | 138 | ||
179 | } else if (answer.equals("dk")) { | 139 | private static void reviewGuesses(@Nonnull Scanner sc, @Nonnull Akiwrapper aw, @Nonnull List<Long> declined) { |
180 | aw.answerCurrentQuestion(Answer.DONT_KNOW); | 140 | for (Guess guess : aw.getGuessesAboveProbability(PROBABILITY_THRESHOLD)) { |
141 | if (guess.getProbability() > 0.85d && !declined.contains(Long.valueOf(guess.getIdLong()))) { | ||
142 | // Checks if this guess complies with the conditions. | ||
181 | 143 | ||
182 | } else if (answer.equals("p")) { | 144 | if (reviewGuess(guess, sc)) { |
183 | aw.answerCurrentQuestion(Answer.PROBABLY); | 145 | // If the user accepts this guess. |
146 | finish(true); | ||
147 | System.exit(0); | ||
148 | } | ||
184 | 149 | ||
185 | } else if (answer.equals("pn")) { | 150 | declined.add(Long.valueOf(guess.getIdLong())); |
186 | aw.answerCurrentQuestion(Answer.PROBABLY_NOT); | 151 | // Registers this guess as rejected. |
152 | } | ||
187 | 153 | ||
188 | } else if (answer.equals("b")) { | 154 | } |
189 | aw.undoAnswer(); | 155 | } |
190 | 156 | ||
191 | } else if (answer.equals("resetkey")) { | 157 | private static void answerQuestion(@Nonnull Scanner sc, @Nonnull Akiwrapper aw) { |
192 | Route.accquireApiKey(); | 158 | boolean answered = false; |
159 | while (!answered) { | ||
160 | // Iterates while the questions remains unanswered. | ||
193 | 161 | ||
194 | } else if (answer.equals("debug")) { | 162 | String answer = sc.nextLine().toLowerCase(); |
195 | System.out.println("Debug information:\n\tCurrent API server: " + aw.getServer().getApiUrl() | ||
196 | + "\n\tCurrent guess count: " + aw.getGuesses().size() | ||
197 | + "\n\tCurrent API server availability: " | ||
198 | + (aw.getServer().isUp() ? "ONLINE" : "OFFILNE")); | ||
199 | continue; | ||
200 | // Displays some debug information. | ||
201 | 163 | ||
202 | } else { | 164 | if (answer.equals("y")) { |
203 | System.out.println( | 165 | aw.answerCurrentQuestion(Answer.YES); |
204 | "Please answer with either YES, NO, DONT KNOW, PROBABLY or PROBABLY NOT or go back one step with BACK."); | ||
205 | continue; | ||
206 | } | ||
207 | 166 | ||
208 | answered = true; | 167 | } else if (answer.equals("n")) { |
209 | // Answers the question. | 168 | aw.answerCurrentQuestion(Answer.NO); |
210 | } | ||
211 | 169 | ||
212 | for (Guess guess : aw.getGuessesAboveProbability(PROBABILITY_THRESHOLD)) { | 170 | } else if (answer.equals("dk")) { |
213 | if (guess.getProbability() > 0.85d && !declined.contains(Long.valueOf(guess.getIdLong()))) { | 171 | aw.answerCurrentQuestion(Answer.DONT_KNOW); |
214 | // Checks if this guess complies with the conditions. | ||
215 | 172 | ||
216 | if (reviewGuess(guess, sc)) { | 173 | } else if (answer.equals("p")) { |
217 | // If the user accepts this guess. | 174 | aw.answerCurrentQuestion(Answer.PROBABLY); |
218 | finish(true); | ||
219 | System.exit(0); | ||
220 | } | ||
221 | 175 | ||
222 | declined.add(Long.valueOf(guess.getIdLong())); | 176 | } else if (answer.equals("pn")) { |
223 | // Registers this guess as rejected. | 177 | aw.answerCurrentQuestion(Answer.PROBABLY_NOT); |
224 | } | ||
225 | 178 | ||
226 | } | 179 | } else if (answer.equals("b")) { |
227 | // Iterates over any available guesses. | 180 | aw.undoAnswer(); |
181 | |||
182 | } else if (answer.equals("resetkey")) { | ||
183 | ApiKey.accquireApiKey(); | ||
184 | |||
185 | } else if (answer.equals("debug")) { | ||
186 | System.out.println("Debug information:\n\tCurrent API server: " | ||
187 | + aw.getServer().getUrl() | ||
188 | + "\n\tCurrent guess count: " | ||
189 | + aw.getGuesses().size()); | ||
190 | continue; | ||
191 | // Displays some debug information. | ||
192 | |||
193 | } else { | ||
194 | System.out.println( | ||
195 | "Please answer with either [Y]ES, [N]O, [D|ONT |K]NOW, [P]ROBABLY or [P|ROBABLY |N]OT or go back one step with [B]ACK."); | ||
196 | continue; | ||
228 | } | 197 | } |
229 | 198 | ||
230 | for (Guess guess : aw.getGuesses()) { | 199 | answered = true; |
231 | if (reviewGuess(guess, sc)) { | 200 | // Answers the question. |
232 | // Reviews all final guesses. | 201 | } |
233 | finish(true); | 202 | } |
234 | System.exit(0); | 203 | |
235 | } | 204 | private static boolean getProfanityFilter(@Nonnull Scanner sc) { |
205 | Boolean result = null; | ||
206 | System.out.println("What's your age? (18)"); | ||
207 | while (result == null) { | ||
208 | String age = sc.nextLine(); | ||
209 | |||
210 | if (age.equals("")) { | ||
211 | result = false; | ||
212 | continue; | ||
236 | } | 213 | } |
237 | 214 | ||
238 | finish(false); | 215 | try { |
239 | // Loses if all guesses are rejected. | 216 | result = Integer.parseInt(age) < 16; |
217 | // Tries to format the given number. | ||
218 | |||
219 | } catch (NumberFormatException e) { | ||
220 | System.out.println("That's not a real age!"); | ||
221 | // In case the given number is not formattable (too big or not a number). | ||
222 | } | ||
223 | } | ||
224 | return result; | ||
225 | } | ||
226 | |||
227 | @Nonnull | ||
228 | private static Language getLanguage(@Nonnull Scanner sc) { | ||
229 | Language result = null; | ||
230 | EnumSet<Language> languages = EnumSet.allOf(Language.class); | ||
231 | // Fetches all available languages. | ||
232 | |||
233 | String unsupportedLanguageMessage = "Sorry, that language isn't supported. Rather try with:" | ||
234 | + languages.stream().map(Enum::toString).collect(Collectors.joining("\n-", "\n-", "")); | ||
235 | // Does some Java 8 magic to pre-prepare the error message. | ||
236 | |||
237 | System.out.println("What's your language? (English)"); | ||
238 | while (result == null) { | ||
239 | String selectedLanguage = sc.nextLine().toLowerCase().trim(); | ||
240 | |||
241 | if (selectedLanguage.equals("")) { | ||
242 | result = Language.ENGLISH; | ||
243 | continue; | ||
244 | } | ||
245 | |||
246 | Language matching = languages.stream() | ||
247 | .filter(l -> l.toString().toLowerCase().equals(selectedLanguage)) | ||
248 | .findAny() | ||
249 | .orElse(null); | ||
250 | |||
251 | if (matching == null) { | ||
252 | System.out.println(unsupportedLanguageMessage); | ||
253 | continue; | ||
254 | } | ||
255 | |||
256 | result = matching; | ||
257 | } | ||
258 | return result; | ||
259 | } | ||
260 | |||
261 | @Nonnull | ||
262 | private static GuessType getGuessType(@Nonnull Scanner sc) { | ||
263 | GuessType result = null; | ||
264 | EnumSet<GuessType> guessTypes = EnumSet.allOf(GuessType.class); | ||
265 | // Fetches all available guess types. | ||
266 | |||
267 | String unsupportedGuessTypeMessage = "Sorry, that guess type isn't supported. Rather try with:" | ||
268 | + guessTypes.stream().map(Enum::toString).collect(Collectors.joining("\n-", "\n-", "")); | ||
269 | // Does some Java 8 magic to pre-prepare the error message. | ||
270 | |||
271 | System.out.println("What will you be guessing? (character)"); | ||
272 | while (result == null) { | ||
273 | String selectedGuessType = sc.nextLine().toLowerCase().trim(); | ||
274 | |||
275 | if (selectedGuessType.equals("")) { | ||
276 | result = GuessType.CHARACTER; | ||
277 | continue; | ||
278 | } | ||
279 | |||
280 | GuessType matching = guessTypes.stream() | ||
281 | .filter(l -> l.toString().toLowerCase().equals(selectedGuessType)) | ||
282 | .findAny() | ||
283 | .orElse(null); | ||
284 | |||
285 | if (matching == null) { | ||
286 | System.out.println(unsupportedGuessTypeMessage); | ||
287 | continue; | ||
288 | } | ||
289 | |||
290 | result = matching; | ||
240 | } | 291 | } |
292 | return result; | ||
241 | } | 293 | } |
242 | 294 | ||
243 | } \ No newline at end of file | 295 | } |
@@ -5,7 +5,7 @@ | |||
5 | 5 | ||
6 | <groupId>com.github.markozajc</groupId> | 6 | <groupId>com.github.markozajc</groupId> |
7 | <artifactId>akiwrapper</artifactId> | 7 | <artifactId>akiwrapper</artifactId> |
8 | <version>1.4.3.1</version> | 8 | <version>1.5</version> |
9 | 9 | ||
10 | <name>Akiwrapper</name> | 10 | <name>Akiwrapper</name> |
11 | <description>A Java API wrapper for Akinator</description> | 11 | <description>A Java API wrapper for Akinator</description> |
@@ -13,12 +13,6 @@ | |||
13 | 13 | ||
14 | <inceptionYear>2017</inceptionYear> | 14 | <inceptionYear>2017</inceptionYear> |
15 | 15 | ||
16 | <scm> | ||
17 | <url>https://github.com/markozajc/Akiwrapper</url> | ||
18 | <connection>scm:git:git://github.com/markozajc/Akiwrapper.git</connection> | ||
19 | <developerConnection>scm:git:ssh://github.com:markozajc/Akiwrapper.git</developerConnection> | ||
20 | </scm> | ||
21 | |||
22 | <licenses> | 16 | <licenses> |
23 | <license> | 17 | <license> |
24 | <name>The GNU General Public License, Version 3.0</name> | 18 | <name>The GNU General Public License, Version 3.0</name> |
@@ -34,6 +28,12 @@ | |||
34 | </developer> | 28 | </developer> |
35 | </developers> | 29 | </developers> |
36 | 30 | ||
31 | <scm> | ||
32 | <url>https://github.com/markozajc/Akiwrapper</url> | ||
33 | <connection>scm:git:git://github.com/markozajc/Akiwrapper.git</connection> | ||
34 | <developerConnection>scm:git:ssh://github.com:markozajc/Akiwrapper.git</developerConnection> | ||
35 | </scm> | ||
36 | |||
37 | <issueManagement> | 37 | <issueManagement> |
38 | <url>https://github.com/markozajc/Akiwrapper/issues</url> | 38 | <url>https://github.com/markozajc/Akiwrapper/issues</url> |
39 | </issueManagement> | 39 | </issueManagement> |
@@ -41,39 +41,103 @@ | |||
41 | <properties> | 41 | <properties> |
42 | <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> | 42 | <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> |
43 | <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> | 43 | <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> |
44 | <junit.version>5.6.2</junit.version> | ||
45 | <slf4j.version>1.7.30</slf4j.version> | ||
44 | </properties> | 46 | </properties> |
45 | 47 | ||
48 | <dependencies> | ||
49 | |||
50 | <dependency> | ||
51 | <groupId>com.jcabi</groupId> | ||
52 | <artifactId>jcabi-xml</artifactId> | ||
53 | <version>0.22.2</version> | ||
54 | </dependency> | ||
55 | |||
56 | <!-- JSON parsing --> | ||
57 | <dependency> | ||
58 | <groupId>org.json</groupId> | ||
59 | <artifactId>json</artifactId> | ||
60 | <version>20190722</version> | ||
61 | </dependency> | ||
62 | |||
63 | <!-- Unit tests --> | ||
64 | <dependency> | ||
65 | <groupId>org.junit.jupiter</groupId> | ||
66 | <artifactId>junit-jupiter-engine</artifactId> | ||
67 | <version>${junit.version}</version> | ||
68 | <scope>test</scope> | ||
69 | </dependency> | ||
70 | <dependency> | ||
71 | <groupId>org.junit.jupiter</groupId> | ||
72 | <artifactId>junit-jupiter-api</artifactId> | ||
73 | <version>${junit.version}</version> | ||
74 | <scope>test</scope> | ||
75 | </dependency> | ||
76 | <dependency> | ||
77 | <groupId>org.junit.jupiter</groupId> | ||
78 | <artifactId>junit-jupiter-params</artifactId> | ||
79 | <version>${junit.version}</version> | ||
80 | <scope>test</scope> | ||
81 | </dependency> | ||
82 | |||
83 | <!-- HTTP requests --> | ||
84 | <dependency> | ||
85 | <groupId>com.konghq</groupId> | ||
86 | <artifactId>unirest-java</artifactId> | ||
87 | <version>3.7.04</version> | ||
88 | </dependency> | ||
89 | |||
90 | <!-- Cache --> | ||
91 | <dependency> | ||
92 | <groupId>com.google.guava</groupId> | ||
93 | <artifactId>guava</artifactId> | ||
94 | <version>29.0-jre</version> | ||
95 | </dependency> | ||
96 | |||
97 | <!-- Logging --> | ||
98 | <dependency> | ||
99 | <groupId>org.slf4j</groupId> | ||
100 | <artifactId>slf4j-api</artifactId> | ||
101 | <version>${slf4j.version}</version> | ||
102 | </dependency> | ||
103 | <dependency> | ||
104 | <groupId>org.slf4j</groupId> | ||
105 | <artifactId>slf4j-simple</artifactId> | ||
106 | <version>${slf4j.version}</version> | ||
107 | <scope>test</scope> | ||
108 | </dependency> | ||
109 | |||
110 | <!-- Annotations --> | ||
111 | <dependency> | ||
112 | <groupId>com.github.spotbugs</groupId> | ||
113 | <artifactId>spotbugs-annotations</artifactId> | ||
114 | <version>4.0.0</version> | ||
115 | </dependency> | ||
116 | |||
117 | <dependency> | ||
118 | <groupId>com.google.code.findbugs</groupId> | ||
119 | <artifactId>jsr305</artifactId> | ||
120 | <version>3.0.2</version> | ||
121 | </dependency> | ||
122 | |||
123 | </dependencies> | ||
124 | |||
46 | <build> | 125 | <build> |
47 | <plugins> | 126 | <plugins> |
48 | 127 | ||
128 | <!-- Compiler --> | ||
49 | <plugin> | 129 | <plugin> |
50 | <groupId>org.apache.maven.plugins</groupId> | ||
51 | <artifactId>maven-compiler-plugin</artifactId> | 130 | <artifactId>maven-compiler-plugin</artifactId> |
52 | <version>3.8.0</version> | 131 | <version>3.8.1</version> |
53 | <configuration> | 132 | <configuration> |
54 | <source>1.8</source> | 133 | <release>11</release> |
55 | <target>1.8</target> | ||
56 | </configuration> | 134 | </configuration> |
57 | </plugin> | 135 | </plugin> |
58 | 136 | ||
137 | <!-- Javadoc --> | ||
59 | <plugin> | 138 | <plugin> |
60 | <groupId>org.apache.maven.plugins</groupId> | ||
61 | <artifactId>maven-source-plugin</artifactId> | ||
62 | <version>3.0.1</version> | ||
63 | <executions> | ||
64 | <execution> | ||
65 | <id>attach-sources</id> | ||
66 | <goals> | ||
67 | <goal>jar</goal> | ||
68 | </goals> | ||
69 | </execution> | ||
70 | </executions> | ||
71 | </plugin> | ||
72 | |||
73 | <plugin> | ||
74 | <groupId>org.apache.maven.plugins</groupId> | ||
75 | <artifactId>maven-javadoc-plugin</artifactId> | 139 | <artifactId>maven-javadoc-plugin</artifactId> |
76 | <version>3.0.1</version> | 140 | <version>3.2.0</version> |
77 | <executions> | 141 | <executions> |
78 | <execution> | 142 | <execution> |
79 | <id>attach-javadocs</id> | 143 | <id>attach-javadocs</id> |
@@ -83,15 +147,14 @@ | |||
83 | </execution> | 147 | </execution> |
84 | </executions> | 148 | </executions> |
85 | <configuration> | 149 | <configuration> |
86 | <additionalparam>-Xdoclint:none</additionalparam> | 150 | <doclint>none</doclint> |
87 | <additionalOptions>-Xdoclint:none</additionalOptions> | ||
88 | </configuration> | 151 | </configuration> |
89 | </plugin> | 152 | </plugin> |
90 | 153 | ||
154 | <!-- Unit tests --> | ||
91 | <plugin> | 155 | <plugin> |
92 | <groupId>org.apache.maven.plugins</groupId> | ||
93 | <artifactId>maven-surefire-plugin</artifactId> | 156 | <artifactId>maven-surefire-plugin</artifactId> |
94 | <version>2.22.1</version> | 157 | <version>2.22.2</version> |
95 | <executions> | 158 | <executions> |
96 | <execution> | 159 | <execution> |
97 | <id>test</id> | 160 | <id>test</id> |
@@ -102,69 +165,70 @@ | |||
102 | </executions> | 165 | </executions> |
103 | </plugin> | 166 | </plugin> |
104 | 167 | ||
168 | <!-- Version checker --> | ||
105 | <plugin> | 169 | <plugin> |
106 | <groupId>org.codehaus.mojo</groupId> | 170 | <groupId>org.codehaus.mojo</groupId> |
107 | <artifactId>versions-maven-plugin</artifactId> | 171 | <artifactId>versions-maven-plugin</artifactId> |
108 | <version>2.7</version> | 172 | <version>2.7</version> |
109 | </plugin> | 173 | </plugin> |
110 | 174 | ||
111 | <plugin> | ||
112 | <groupId>org.apache.maven.plugins</groupId> | ||
113 | <artifactId>maven-gpg-plugin</artifactId> | ||
114 | <version>1.6</version> | ||
115 | <executions> | ||
116 | <execution> | ||
117 | <id>sign-artifacts</id> | ||
118 | <phase>verify</phase> | ||
119 | <goals> | ||
120 | <goal>sign</goal> | ||
121 | </goals> | ||
122 | </execution> | ||
123 | </executions> | ||
124 | </plugin> | ||
125 | |||
126 | <plugin> | ||
127 | <groupId>org.sonatype.plugins</groupId> | ||
128 | <artifactId>nexus-staging-maven-plugin</artifactId> | ||
129 | <version>1.6.7</version> | ||
130 | <extensions>true</extensions> | ||
131 | <configuration> | ||
132 | <serverId>ossrh</serverId> | ||
133 | <nexusUrl>https://oss.sonatype.org/</nexusUrl> | ||
134 | <autoReleaseAfterClose>false</autoReleaseAfterClose> | ||
135 | </configuration> | ||
136 | </plugin> | ||
137 | |||
138 | </plugins> | 175 | </plugins> |
139 | </build> | 176 | </build> |
140 | 177 | ||
141 | <dependencies> | 178 | <profiles> |
142 | 179 | <profile> | |
143 | <dependency> | 180 | <id>release</id> |
144 | <groupId>org.json</groupId> | 181 | <build> |
145 | <artifactId>json</artifactId> | 182 | <plugins> |
146 | <version>20180813</version> | 183 | |
147 | </dependency> | 184 | <!-- Source --> |
148 | 185 | <plugin> | |
149 | <dependency> | 186 | <groupId>org.apache.maven.plugins</groupId> |
150 | <groupId>com.google.code.findbugs</groupId> | 187 | <artifactId>maven-source-plugin</artifactId> |
151 | <artifactId>jsr305</artifactId> | 188 | <version>3.2.1</version> |
152 | <version>3.0.2</version> | 189 | <executions> |
153 | </dependency> | 190 | <execution> |
154 | 191 | <id>attach-sources</id> | |
155 | <dependency> | 192 | <goals> |
156 | <groupId>org.junit.jupiter</groupId> | 193 | <goal>jar</goal> |
157 | <artifactId>junit-jupiter-engine</artifactId> | 194 | </goals> |
158 | <version>5.5.1</version> | 195 | </execution> |
159 | <scope>test</scope> | 196 | </executions> |
160 | </dependency> | 197 | </plugin> |
161 | 198 | ||
162 | <dependency> | 199 | <!-- Signing --> |
163 | <groupId>com.github.spotbugs</groupId> | 200 | <plugin> |
164 | <artifactId>spotbugs-annotations</artifactId> | 201 | <groupId>org.apache.maven.plugins</groupId> |
165 | <version>3.1.11</version> | 202 | <artifactId>maven-gpg-plugin</artifactId> |
166 | </dependency> | 203 | <version>1.6</version> |
167 | 204 | <executions> | |
168 | </dependencies> | 205 | <execution> |
206 | <id>sign-artifacts</id> | ||
207 | <phase>verify</phase> | ||
208 | <goals> | ||
209 | <goal>sign</goal> | ||
210 | </goals> | ||
211 | </execution> | ||
212 | </executions> | ||
213 | </plugin> | ||
214 | |||
215 | <!-- Nexus deployment --> | ||
216 | <plugin> | ||
217 | <groupId>org.sonatype.plugins</groupId> | ||
218 | <artifactId>nexus-staging-maven-plugin</artifactId> | ||
219 | <version>1.6.8</version> | ||
220 | <extensions>true</extensions> | ||
221 | <configuration> | ||
222 | <serverId>ossrh</serverId> | ||
223 | <nexusUrl>https://oss.sonatype.org/</nexusUrl> | ||
224 | <autoReleaseAfterClose>false</autoReleaseAfterClose> | ||
225 | </configuration> | ||
226 | </plugin> | ||
227 | |||
228 | </plugins> | ||
229 | </build> | ||
230 | </profile> | ||
231 | |||
232 | </profiles> | ||
169 | 233 | ||
170 | </project> \ No newline at end of file | 234 | </project> \ No newline at end of file |
diff --git a/src/main/java/com/markozajc/akiwrapper/Akiwrapper.java b/src/main/java/com/markozajc/akiwrapper/Akiwrapper.java index 722e57e..90f2876 100644 --- a/src/main/java/com/markozajc/akiwrapper/Akiwrapper.java +++ b/src/main/java/com/markozajc/akiwrapper/Akiwrapper.java | |||
@@ -1,6 +1,5 @@ | |||
1 | package com.markozajc.akiwrapper; | 1 | package com.markozajc.akiwrapper; |
2 | 2 | ||
3 | import java.io.IOException; | ||
4 | import java.util.List; | 3 | import java.util.List; |
5 | import java.util.stream.Collectors; | 4 | import java.util.stream.Collectors; |
6 | 5 | ||
@@ -20,9 +19,10 @@ import com.markozajc.akiwrapper.core.entities.Server; | |||
20 | public interface Akiwrapper { | 19 | public interface Akiwrapper { |
21 | 20 | ||
22 | /** | 21 | /** |
23 | * An enum used to represent an answer to Akinator's question | 22 | * An enum used to represent an answer to Akinator's question. |
24 | */ | 23 | */ |
25 | public enum Answer { | 24 | public enum Answer { |
25 | |||
26 | /** | 26 | /** |
27 | * Answers with "yes" (positive) | 27 | * Answers with "yes" (positive) |
28 | */ | 28 | */ |
@@ -72,15 +72,12 @@ public interface Akiwrapper { | |||
72 | * the answer | 72 | * the answer |
73 | * | 73 | * |
74 | * @return the latest question or null if an answer was found | 74 | * @return the latest question or null if an answer was found |
75 | * @throws IOException | ||
76 | * if something goes wrong | ||
77 | */ | 75 | */ |
78 | @Nullable | 76 | @Nullable |
79 | Question answerCurrentQuestion(Answer answer) throws IOException; | 77 | Question answerCurrentQuestion(Answer answer); |
80 | 78 | ||
81 | /** | 79 | /** |
82 | * Goes one step backwards (just like {@link #answerCurrentQuestion(Answer)}, except | 80 | * Goes one question backwards.<br> |
83 | * it goes back instead of forward).<br> | ||
84 | * For example, if {@link #getCurrentQuestion()} returns a question on step | 81 | * For example, if {@link #getCurrentQuestion()} returns a question on step |
85 | * {@code 5}, calling this command will make {@link #getCurrentQuestion()} return the | 82 | * {@code 5}, calling this command will make {@link #getCurrentQuestion()} return the |
86 | * question from step {@code 4}. You can call this as many times as you want.<br> | 83 | * question from step {@code 4}. You can call this as many times as you want.<br> |
@@ -88,16 +85,15 @@ public interface Akiwrapper { | |||
88 | * question on step {@code 0}, calling this will return {@code null} and nothing will | 85 | * question on step {@code 0}, calling this will return {@code null} and nothing will |
89 | * actually be changed!<br> | 86 | * actually be changed!<br> |
90 | * This will also return {@code null} if {@link #getCurrentQuestion()} returns | 87 | * This will also return {@code null} if {@link #getCurrentQuestion()} returns |
91 | * {@code null},</strong> | 88 | * {@code null} as well.</strong> |
92 | * | 89 | * |
93 | * @return the past message | 90 | * @return the past message |
94 | * @throws IOException | ||
95 | */ | 91 | */ |
96 | @Nullable | 92 | @Nullable |
97 | Question undoAnswer() throws IOException; | 93 | Question undoAnswer(); |
98 | 94 | ||
99 | /** | 95 | /** |
100 | * Returns current question. You can answer it with | 96 | * Returns the current question. You can answer it with |
101 | * {@link #answerCurrentQuestion(Answer)}. If there are no more questions left, this | 97 | * {@link #answerCurrentQuestion(Answer)}. If there are no more questions left, this |
102 | * will return {@code null}. | 98 | * will return {@code null}. |
103 | * | 99 | * |
@@ -108,15 +104,14 @@ public interface Akiwrapper { | |||
108 | 104 | ||
109 | /** | 105 | /** |
110 | * @return an unmodifiable list of Akinator's guesses, empty if there are no guesses | 106 | * @return an unmodifiable list of Akinator's guesses, empty if there are no guesses |
111 | * @throws IOException | 107 | * |
112 | * if API call isn't successful | ||
113 | * @see Akiwrapper#getGuessesAboveProbability(double) | 108 | * @see Akiwrapper#getGuessesAboveProbability(double) |
114 | */ | 109 | */ |
115 | @Nonnull | 110 | @Nonnull |
116 | List<Guess> getGuesses() throws IOException; | 111 | List<Guess> getGuesses(); |
117 | 112 | ||
118 | /** | 113 | /** |
119 | * @return the API server this instance of Akiwrapper uses | 114 | * @return the API server this instance of Akiwrapper uses. |
120 | */ | 115 | */ |
121 | @Nonnull | 116 | @Nonnull |
122 | Server getServer(); | 117 | Server getServer(); |
@@ -124,14 +119,15 @@ public interface Akiwrapper { | |||
124 | /** | 119 | /** |
125 | * @param probability | 120 | * @param probability |
126 | * probability threshold | 121 | * probability threshold |
122 | * | ||
127 | * @return a list of Akinator's guesses with probability above the specified | 123 | * @return a list of Akinator's guesses with probability above the specified |
128 | * probability threshold. | 124 | * probability threshold. |
129 | * @throws IOException | 125 | * |
130 | * @see Akiwrapper#getGuesses() | 126 | * @see Akiwrapper#getGuesses() |
131 | */ | 127 | */ |
132 | @SuppressWarnings("null") | 128 | @SuppressWarnings("null") |
133 | @Nonnull | 129 | @Nonnull |
134 | default List<Guess> getGuessesAboveProbability(double probability) throws IOException { | 130 | default List<Guess> getGuessesAboveProbability(double probability) { |
135 | return getGuesses().stream().filter(g -> g.getProbability() > probability).collect(Collectors.toList()); | 131 | return getGuesses().stream().filter(g -> g.getProbability() > probability).collect(Collectors.toList()); |
136 | } | 132 | } |
137 | } \ No newline at end of file | 133 | } |
diff --git a/src/main/java/com/markozajc/akiwrapper/AkiwrapperBuilder.java b/src/main/java/com/markozajc/akiwrapper/AkiwrapperBuilder.java index 1db82bd..e658135 100644 --- a/src/main/java/com/markozajc/akiwrapper/AkiwrapperBuilder.java +++ b/src/main/java/com/markozajc/akiwrapper/AkiwrapperBuilder.java | |||
@@ -2,74 +2,105 @@ package com.markozajc.akiwrapper; | |||
2 | 2 | ||
3 | import javax.annotation.Nonnull; | 3 | import javax.annotation.Nonnull; |
4 | 4 | ||
5 | import org.slf4j.Logger; | ||
6 | import org.slf4j.LoggerFactory; | ||
7 | |||
5 | import com.markozajc.akiwrapper.core.entities.AkiwrapperMetadata; | 8 | import com.markozajc.akiwrapper.core.entities.AkiwrapperMetadata; |
6 | import com.markozajc.akiwrapper.core.entities.Server; | 9 | import com.markozajc.akiwrapper.core.entities.Server; |
10 | import com.markozajc.akiwrapper.core.entities.Server.GuessType; | ||
7 | import com.markozajc.akiwrapper.core.entities.Server.Language; | 11 | import com.markozajc.akiwrapper.core.entities.Server.Language; |
12 | import com.markozajc.akiwrapper.core.entities.ServerList; | ||
8 | import com.markozajc.akiwrapper.core.entities.impl.mutable.MutableAkiwrapperMetadata; | 13 | import com.markozajc.akiwrapper.core.entities.impl.mutable.MutableAkiwrapperMetadata; |
9 | import com.markozajc.akiwrapper.core.exceptions.ServerGroupUnavailableException; | 14 | import com.markozajc.akiwrapper.core.exceptions.ServerNotFoundException; |
15 | import com.markozajc.akiwrapper.core.exceptions.ServerUnavailableException; | ||
10 | import com.markozajc.akiwrapper.core.impl.AkiwrapperImpl; | 16 | import com.markozajc.akiwrapper.core.impl.AkiwrapperImpl; |
17 | import com.markozajc.akiwrapper.core.utils.Servers; | ||
11 | 18 | ||
12 | /** | 19 | /** |
13 | * A class used for building a new Akinator object. | 20 | * A class used to build an {@link Akiwrapper} object. It allows you to set various |
21 | * values before building it in a method chaining fashion. | ||
14 | * | 22 | * |
15 | * @author Marko Zajc | 23 | * @author Marko Zajc |
16 | */ | 24 | */ |
17 | public class AkiwrapperBuilder extends MutableAkiwrapperMetadata { | 25 | public class AkiwrapperBuilder extends MutableAkiwrapperMetadata { |
18 | 26 | ||
27 | private static final Logger LOG = LoggerFactory.getLogger(AkiwrapperBuilder.class); | ||
28 | |||
19 | /** | 29 | /** |
20 | * Creates a new AkiwrapperBuilder object. The default server used is the first | 30 | * Creates a new AkiwrapperBuilder object. The default server used is the first |
21 | * available server. If a value is not changed, a constant default from | 31 | * available server. If a value is not changed, a constant default from |
22 | * {@link AkiwrapperMetadata} is used. | 32 | * {@link AkiwrapperMetadata} is used. |
23 | */ | 33 | */ |
24 | @SuppressWarnings("null") | ||
25 | public AkiwrapperBuilder() { | 34 | public AkiwrapperBuilder() { |
26 | super(AkiwrapperMetadata.DEFAULT_NAME, AkiwrapperMetadata.DEFAULT_USER_AGENT, null, | 35 | super(null, AkiwrapperMetadata.DEFAULT_FILTER_PROFANITY, AkiwrapperMetadata.DEFAULT_LOCALIZATION, |
27 | AkiwrapperMetadata.DEFAULT_FILTER_PROFANITY, AkiwrapperMetadata.DEFAULT_LOCALIZATION); | 36 | AkiwrapperMetadata.DEFAULT_GUESS_TYPE); |
28 | } | 37 | } |
29 | 38 | ||
30 | @Override | 39 | @Override |
31 | public AkiwrapperBuilder setName(String name) { | 40 | public AkiwrapperBuilder setServer(Server server) { |
32 | super.setName(name); | 41 | super.setServer(server); |
33 | |||
34 | return this; | ||
35 | } | ||
36 | |||
37 | @Override | ||
38 | public AkiwrapperBuilder setUserAgent(String userAgent) { | ||
39 | super.setUserAgent(userAgent); | ||
40 | 42 | ||
41 | return this; | 43 | return this; |
42 | } | 44 | } |
43 | 45 | ||
44 | @Override | 46 | @Override |
45 | public AkiwrapperBuilder setServer(Server server) { | 47 | public AkiwrapperBuilder setFilterProfanity(boolean filterProfanity) { |
46 | super.setServer(server); | 48 | super.setFilterProfanity(filterProfanity); |
47 | 49 | ||
48 | return this; | 50 | return this; |
49 | } | 51 | } |
50 | 52 | ||
51 | @Override | 53 | @Override |
52 | public AkiwrapperBuilder setFilterProfanity(boolean filterProfanity) { | 54 | public AkiwrapperBuilder setLanguage(Language localization) { |
53 | super.setFilterProfanity(filterProfanity); | 55 | super.setLanguage(localization); |
54 | 56 | ||
55 | return this; | 57 | return this; |
56 | } | 58 | } |
57 | 59 | ||
58 | @Override | 60 | @Override |
59 | public AkiwrapperBuilder setLocalization(Language localization) { | 61 | public AkiwrapperBuilder setGuessType(GuessType guessType) { |
60 | super.setLocalization(localization); | 62 | super.setGuessType(guessType); |
61 | 63 | ||
62 | return this; | 64 | return this; |
63 | } | 65 | } |
64 | 66 | ||
65 | /** | 67 | /** |
66 | * @return a new {@link Akiwrapper} instance that will use all set preferences | 68 | * @return a new {@link Akiwrapper} instance that will use all set preferences |
67 | * @throws ServerGroupUnavailableException | 69 | * |
68 | * in case no servers of that language are available | 70 | * @throws ServerNotFoundException |
71 | * if no server with that {@link Language} and {@link GuessType} is | ||
72 | * available. | ||
69 | */ | 73 | */ |
70 | @Nonnull | 74 | @Nonnull |
71 | public Akiwrapper build() { | 75 | public Akiwrapper build() throws ServerNotFoundException { |
72 | return new AkiwrapperImpl(this); | 76 | Server server = findServer(); |
77 | if (server instanceof ServerList) { | ||
78 | ServerList serverList = (ServerList) server; | ||
79 | int count = serverList.getRemainingSize() + 1; | ||
80 | do { | ||
81 | LOG.debug("Using server {} out of {} from the list.", count - serverList.getRemainingSize(), count); | ||
82 | try { | ||
83 | return new AkiwrapperImpl(server, this.filterProfanity); | ||
84 | } catch (ServerUnavailableException e) { // NOSONAR v | ||
85 | LOG.debug("Server seems to be down."); | ||
86 | // We can safely ignore this, let's just iterate to the next instance. | ||
87 | } catch (RuntimeException e) { | ||
88 | LOG.warn("Failed to construct an instance, trying the next available server", e); | ||
89 | } | ||
90 | } while (serverList.next()); | ||
91 | throw new ServerUnavailableException("AW-KO MULTIPLE FAILS"); | ||
92 | } else { | ||
93 | LOG.debug("Given Server is not a ServerList, only attempting to build once."); | ||
94 | return new AkiwrapperImpl(server, this.filterProfanity); | ||
95 | } | ||
96 | } | ||
97 | |||
98 | @Nonnull | ||
99 | private Server findServer() throws ServerNotFoundException { | ||
100 | Server server = this.getServer(); | ||
101 | if (server == null) | ||
102 | server = Servers.findServers(this.getLanguage(), this.getGuessType()); | ||
103 | return server; | ||
73 | } | 104 | } |
74 | 105 | ||
75 | } | 106 | } |
diff --git a/src/main/java/com/markozajc/akiwrapper/core/Route.java b/src/main/java/com/markozajc/akiwrapper/core/Route.java index 494ad15..2b9ecfb 100644 --- a/src/main/java/com/markozajc/akiwrapper/core/Route.java +++ b/src/main/java/com/markozajc/akiwrapper/core/Route.java | |||
@@ -1,90 +1,72 @@ | |||
1 | package com.markozajc.akiwrapper.core; | 1 | package com.markozajc.akiwrapper.core; |
2 | 2 | ||
3 | import java.io.IOException; | ||
4 | import java.io.UnsupportedEncodingException; | 3 | import java.io.UnsupportedEncodingException; |
5 | import java.net.URL; | ||
6 | import java.net.URLConnection; | ||
7 | import java.net.URLEncoder; | 4 | import java.net.URLEncoder; |
8 | import java.nio.charset.StandardCharsets; | ||
9 | import java.util.regex.Matcher; | 5 | import java.util.regex.Matcher; |
10 | import java.util.regex.Pattern; | 6 | import java.util.regex.Pattern; |
11 | 7 | ||
12 | import javax.annotation.Nonnull; | 8 | import javax.annotation.Nonnull; |
13 | import javax.annotation.Nullable; | 9 | import javax.annotation.Nullable; |
14 | 10 | ||
11 | import org.json.JSONException; | ||
15 | import org.json.JSONObject; | 12 | import org.json.JSONObject; |
13 | import org.slf4j.Logger; | ||
14 | import org.slf4j.LoggerFactory; | ||
16 | 15 | ||
17 | import com.markozajc.akiwrapper.core.entities.AkiwrapperMetadata; | ||
18 | import com.markozajc.akiwrapper.core.entities.Server; | ||
19 | import com.markozajc.akiwrapper.core.entities.Status; | 16 | import com.markozajc.akiwrapper.core.entities.Status; |
20 | import com.markozajc.akiwrapper.core.entities.Status.Level; | 17 | import com.markozajc.akiwrapper.core.entities.Status.Level; |
18 | import com.markozajc.akiwrapper.core.entities.impl.immutable.ApiKey; | ||
21 | import com.markozajc.akiwrapper.core.entities.impl.immutable.StatusImpl; | 19 | import com.markozajc.akiwrapper.core.entities.impl.immutable.StatusImpl; |
22 | import com.markozajc.akiwrapper.core.exceptions.ServerUnavailableException; | 20 | import com.markozajc.akiwrapper.core.exceptions.ServerUnavailableException; |
23 | import com.markozajc.akiwrapper.core.exceptions.StatusException; | 21 | import com.markozajc.akiwrapper.core.exceptions.StatusException; |
24 | import com.markozajc.akiwrapper.core.impl.AkiwrapperImpl.Token; | 22 | import com.markozajc.akiwrapper.core.impl.AkiwrapperImpl.Token; |
25 | import com.markozajc.akiwrapper.core.utils.HTTPUtils; | ||
26 | 23 | ||
27 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; | 24 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; |
25 | import kong.unirest.Unirest; | ||
26 | import kong.unirest.UnirestInstance; | ||
28 | 27 | ||
29 | /** | 28 | /** |
30 | * A class defining various API endpoints (routes). | 29 | * A class defining various API endpoints. It is capable of building such |
30 | * {@link Route}s into {@link Request}s, which can then easily be executed and read. | ||
31 | * | 31 | * |
32 | * @author Marko Zajc | 32 | * @author Marko Zajc |
33 | */ | 33 | */ |
34 | public class Route { | 34 | public final class Route { |
35 | |||
36 | private static final Pattern FILTER_ARGUMENT_PATTERN = Pattern.compile("\\{FILTER\\}"); | ||
37 | |||
38 | private static class ApiKey { | ||
39 | |||
40 | private static final String FORMAT = "frontaddr=%s&uid_ext_session=%s"; | ||
41 | |||
42 | @Nonnull | ||
43 | private final String sessionUid; | ||
44 | @Nonnull | ||
45 | private final String frontAddress; | ||
46 | |||
47 | ApiKey(@Nonnull String sessionUid, @Nonnull String frontAddress) { | ||
48 | this.sessionUid = sessionUid; | ||
49 | this.frontAddress = frontAddress; | ||
50 | } | ||
51 | |||
52 | @SuppressWarnings("null") | ||
53 | @Nonnull | ||
54 | String compile() { | ||
55 | try { | ||
56 | return String.format(FORMAT, URLEncoder.encode(this.frontAddress, "UTF-8"), this.sessionUid); | ||
57 | } catch (UnsupportedEncodingException e) { | ||
58 | return ""; // never throws | ||
59 | } | ||
60 | } | ||
61 | 35 | ||
36 | /** | ||
37 | * A public {@link UnirestInstance} with all required default headers set. | ||
38 | */ | ||
39 | public static final UnirestInstance UNIREST; | ||
40 | |||
41 | static { | ||
42 | UNIREST = Unirest.spawnInstance(); | ||
43 | UNIREST.config() | ||
44 | .setDefaultHeader("Accept", | ||
45 | "text/javascript, application/javascript, application/ecmascript, application/x-ecmascript, */*. q=0.01") | ||
46 | .setDefaultHeader("Accept-Language", "en-US,en.q=0.9,ar.q=0.8") | ||
47 | .setDefaultHeader("X-Requested-With", "XMLHttpRequest") | ||
48 | .setDefaultHeader("Sec-Fetch-Dest", "empty") | ||
49 | .setDefaultHeader("Sec-Fetch-Mode", "cors") | ||
50 | .setDefaultHeader("Sec-Fetch-Site", "same-origin") | ||
51 | .setDefaultHeader("Connection", "keep-alive") | ||
52 | .setDefaultHeader("User-Agent", | ||
53 | "Mozilla/5.0 (Windows NT 10.0. Win64. x64) AppleWebKit/537.36" + | ||
54 | "(KHTML, like Gecko) Chrome/81.0.4044.92 Safari/537.36") | ||
55 | .setDefaultHeader("Referer", "https://en.akinator.com/game") | ||
56 | .cookieSpec("ignore"); | ||
57 | // Configures necessary headers | ||
58 | // https://github.com/markozajc/Akiwrapper/issues/14#issuecomment-612255613 | ||
59 | // Also disable cookies because they aren't necessary. | ||
60 | // NB: use "standard" if they become necessary. Default value causes log spam and | ||
61 | // probably doesn't even store cookies right. | ||
62 | } | 62 | } |
63 | 63 | ||
64 | private static final String BASE_AKINATOR_URL = "https://en.akinator.com"; | ||
65 | // The base Akinator URL, used for scraping various elements (and not for the API | ||
66 | // calls) | ||
67 | |||
68 | private static final Pattern API_KEY_PATTERN = Pattern | ||
69 | .compile("var uid_ext_session = '(.*)'\\;\\n.*var frontaddr = '(.*)'\\;"); | ||
70 | |||
71 | /** | 64 | /** |
72 | * Scraps the API key from Akinator's website and stores it for later use. | 65 | * The base Akinator URL, used for scraping and some API calls. |
73 | * | ||
74 | * @return the API key | ||
75 | * @throws IOException | ||
76 | * in case the API key can't be scraped | ||
77 | */ | 66 | */ |
78 | @SuppressWarnings("null") | 67 | public static final String BASE_AKINATOR_URL = "https://en.akinator.com"; |
79 | public static ApiKey accquireApiKey() throws IOException { | 68 | private static final String SERVER_DOWN_STATUS_MESSAGE = "server down"; |
80 | Matcher matcher = API_KEY_PATTERN.matcher( | 69 | private static final Pattern FILTER_ARGUMENT_PATTERN = Pattern.compile("\\{FILTER\\}"); |
81 | new String(HTTPUtils.read(new URL(BASE_AKINATOR_URL + "/game").openConnection()), StandardCharsets.UTF_8)); | ||
82 | if (!matcher.find()) | ||
83 | throw new IOException( | ||
84 | "Couldn't scrap the API key! Please consider opening a new ticket at https://github.com/markozajc/Akiwrapper/issues."); | ||
85 | |||
86 | return new ApiKey(matcher.group(1), matcher.group(2)); | ||
87 | } | ||
88 | 70 | ||
89 | /** | 71 | /** |
90 | * Whether to run status checks on {@link Request#getJSON()} by default. Setting this | 72 | * Whether to run status checks on {@link Request#getJSON()} by default. Setting this |
@@ -95,14 +77,22 @@ public class Route { | |||
95 | public static boolean defaultRunChecks = true; // NOSONAR | 77 | public static boolean defaultRunChecks = true; // NOSONAR |
96 | 78 | ||
97 | /** | 79 | /** |
98 | * Creates a new session for further gameplay. Parameters: | 80 | * Creates a new session for further gameplay.<br> |
81 | * <b>Caution!</b> Because this endpoint uses a static hostname, you <u>must</u> pass | ||
82 | * an empty string to {@code baseUrl} of | ||
83 | * {@link #getRequest(String, boolean, String...)} <br> | ||
84 | * Parameters: | ||
99 | * <ol> | 85 | * <ol> |
100 | * <li>Player's name</li> | 86 | * <li>Current time in milliseconds</li> |
87 | * <li>API server's URL</li> | ||
101 | * </ol> | 88 | * </ol> |
102 | */ | 89 | */ |
103 | public static final Route NEW_SESSION = new Route(1, | 90 | public static final Route NEW_SESSION = |
104 | "new_session?partner=1&player=%s&constraint=ETAT%%3C%%3E%%27AV%%27&{API_KEY}&soft_constraint={FILTER}&question_filter={FILTER}", | 91 | new Route(1, |
105 | "ETAT=%%27EN%%27", "cat=1"); | 92 | BASE_AKINATOR_URL + |
93 | "/new_session?partner=1&player=website-desktop&constraint=ETAT%%3C%%3E%%27AV%%27&{API_KEY}" + | ||
94 | "&soft_constraint={FILTER}&question_filter={FILTER}&_=%s&urlApiWs=%s", | ||
95 | "ETAT=%%27EN%%27", "cat=1"); | ||
106 | 96 | ||
107 | /** | 97 | /** |
108 | * Answers a question. Parameters: | 98 | * Answers a question. Parameters: |
@@ -111,7 +101,7 @@ public class Route { | |||
111 | * <li>Answer's ID</li> | 101 | * <li>Answer's ID</li> |
112 | * </ol> | 102 | * </ol> |
113 | */ | 103 | */ |
114 | public static final Route ANSWER = new Route(2, "answer?step=%s&answer=%s", "&question_filter=cat=1"); | 104 | public static final Route ANSWER = new Route(2, "/answer?step=%s&answer=%s", "&question_filter=cat=1"); |
115 | 105 | ||
116 | /** | 106 | /** |
117 | * Cancels (undoes) an answer. Parameters: | 107 | * Cancels (undoes) an answer. Parameters: |
@@ -119,7 +109,8 @@ public class Route { | |||
119 | * <li>Current step</li> | 109 | * <li>Current step</li> |
120 | * </ol> | 110 | * </ol> |
121 | */ | 111 | */ |
122 | public static final Route CANCEL_ANSWER = new Route(1, "cancel_answer?step=%s&answer=-1", "&question_filter=cat=1"); | 112 | public static final Route CANCEL_ANSWER = |
113 | new Route(1, "/cancel_answer?step=%s&answer=-1", "&question_filter=cat=1"); | ||
123 | 114 | ||
124 | /** | 115 | /** |
125 | * Lists all available guesses. Parameters: | 116 | * Lists all available guesses. Parameters: |
@@ -127,77 +118,78 @@ public class Route { | |||
127 | * <li>Current step</li> | 118 | * <li>Current step</li> |
128 | * </ol> | 119 | * </ol> |
129 | */ | 120 | */ |
130 | public static final Route LIST = new Route(1, "list?mode_question=0&step=%s"); | 121 | public static final Route LIST = new Route(1, "/list?mode_question=0&step=%s"); |
122 | |||
123 | @Nonnull | ||
124 | private final String path; | ||
125 | @Nonnull | ||
126 | private final String[] filterArguments; | ||
127 | |||
128 | private final int parametersQuantity; | ||
129 | |||
130 | private Route(int parameters, @Nonnull String path) { | ||
131 | this(parameters, path, new String[0]); | ||
132 | } | ||
133 | |||
134 | private Route(int parameters, @Nonnull String path, @Nonnull String... filterArguments) { | ||
135 | this.path = path; | ||
136 | this.filterArguments = filterArguments.clone(); | ||
137 | this.parametersQuantity = parameters; | ||
138 | } | ||
131 | 139 | ||
132 | /** | 140 | /** |
133 | * Tests whether a response is a successful or a failed one. | 141 | * Tests whether a response is a successful or a failed one. |
134 | * | 142 | * |
135 | * @param response | 143 | * @param response |
136 | * the response to test | 144 | * the response to test |
137 | * @param server | 145 | * |
138 | * the {@link Server} to include in a {@link ServerUnavailableException}, | ||
139 | * if it occurs | ||
140 | * @throws ServerUnavailableException | 146 | * @throws ServerUnavailableException |
141 | * throws if the status is equal to {@link Level#ERROR} and the error | 147 | * throws if the status is equal to {@link Level#ERROR} and the error |
142 | * message hints that the server is down | 148 | * message hints that the server is down |
143 | * @throws StatusException | 149 | * @throws StatusException |
144 | * thrown if the status is equal to {@link Level#ERROR} | 150 | * thrown if the status is equal to {@link Level#ERROR} |
145 | */ | 151 | */ |
146 | public static void testResponse(JSONObject response, Server server) { | 152 | public static void testResponse(@Nonnull JSONObject response) { |
147 | Status compl = new StatusImpl(response); | 153 | Status completion = new StatusImpl(response); |
148 | if (compl.getLevel().equals(Level.ERROR)) { | 154 | if (completion.getLevel() == Status.Level.ERROR) { |
149 | if (compl.getReason().equalsIgnoreCase("server down")) { | 155 | if (SERVER_DOWN_STATUS_MESSAGE.equalsIgnoreCase(completion.getReason())) |
150 | throw new ServerUnavailableException(server); | 156 | throw new ServerUnavailableException(completion); |
151 | } | ||
152 | 157 | ||
153 | throw new StatusException(compl); | 158 | throw new StatusException(completion); |
154 | } | 159 | } |
155 | } | 160 | } |
156 | 161 | ||
157 | private final String path; | ||
158 | private final String[] filterArguments; | ||
159 | private String userAgent = AkiwrapperMetadata.DEFAULT_USER_AGENT; | ||
160 | |||
161 | private final int parametersQuantity; | ||
162 | |||
163 | private Route(int parameters, String path) { | ||
164 | this(parameters, path, new String[0]); | ||
165 | } | ||
166 | |||
167 | private Route(int parameters, String path, String... filterArguments) { | ||
168 | this.path = path; | ||
169 | this.filterArguments = filterArguments.clone(); | ||
170 | this.parametersQuantity = parameters; | ||
171 | } | ||
172 | |||
173 | /** | 162 | /** |
174 | * Creates a request for this route that can later be called and converted into a | 163 | * Constructs a {@link Request} for a route that can later be executed and converted |
175 | * {@link JSONObject}. | 164 | * into a {@link JSONObject}. |
176 | * | 165 | * |
177 | * @param baseUrl | 166 | * @param baseUrl |
178 | * base (API's) URL | ||
179 | * @param filterProfanity | 167 | * @param filterProfanity |
180 | * whether to filter profanity. Akinator's website will automatically | ||
181 | * enable that if you choose an age below 16 | ||
182 | * @param token | 168 | * @param token |
183 | * the token used for session authentication | ||
184 | * @param parameters | 169 | * @param parameters |
185 | * parameters to pass to the route (parameters are specified in that | 170 | * |
186 | * Route's JavaDoc) | 171 | * @return a {@link Request}. |
187 | * @return a callable request | 172 | * |
188 | * @throws IOException | ||
189 | * @throws IllegalArgumentException | 173 | * @throws IllegalArgumentException |
190 | * if you have passed too little parameters | 174 | * if you have passed too little parameters. |
191 | */ | 175 | */ |
192 | public Request getRequest(String baseUrl, boolean filterProfanity, @Nullable Token token, String... parameters) | 176 | @Nonnull |
193 | throws IOException { | 177 | public Request getRequest(@Nonnull String baseUrl, boolean filterProfanity, @Nullable Token token, |
178 | @Nonnull String... parameters) { | ||
194 | if (parameters.length < this.parametersQuantity) | 179 | if (parameters.length < this.parametersQuantity) |
195 | throw new IllegalArgumentException( | 180 | throw new IllegalArgumentException("Insufficient parameters; Expected " + this.parametersQuantity + |
196 | "Insufficient parameters; Expected " + this.parametersQuantity + ", got " + parameters.length); | 181 | ", got " + |
182 | parameters.length); | ||
197 | 183 | ||
198 | String[] encodedParams = new String[parameters.length]; | 184 | String[] encodedParams = new String[parameters.length]; |
199 | for (int i = 0; i < parameters.length; i++) | 185 | for (int i = 0; i < parameters.length; i++) { |
200 | encodedParams[i] = URLEncoder.encode(parameters[i], "UTF-8"); | 186 | try { |
187 | encodedParams[i] = URLEncoder.encode(parameters[i], "UTF-8"); | ||
188 | } catch (UnsupportedEncodingException e) { | ||
189 | // Can not occur | ||
190 | throw new RuntimeException(e); | ||
191 | } | ||
192 | } | ||
201 | 193 | ||
202 | String formattedPath = this.path; | 194 | String formattedPath = this.path; |
203 | 195 | ||
@@ -209,7 +201,7 @@ public class Route { | |||
209 | matcher.appendTail(sb); | 201 | matcher.appendTail(sb); |
210 | formattedPath = sb.toString(); | 202 | formattedPath = sb.toString(); |
211 | 203 | ||
212 | formattedPath = formattedPath.replace("{API_KEY}", accquireApiKey().compile().replace("%", "%%")); | 204 | formattedPath = formattedPath.replace("{API_KEY}", ApiKey.accquireApiKey().compile().replace("%", "%%")); |
213 | 205 | ||
214 | formattedPath = String.format(formattedPath, (Object[]) encodedParams); | 206 | formattedPath = String.format(formattedPath, (Object[]) encodedParams); |
215 | 207 | ||
@@ -219,122 +211,82 @@ public class Route { | |||
219 | if (token != null) | 211 | if (token != null) |
220 | formattedPath = formattedPath + token.compile(); | 212 | formattedPath = formattedPath + token.compile(); |
221 | 213 | ||
222 | return new Request(new URL(baseUrl + formattedPath), this.userAgent, jQueryCallback); | 214 | return new Request(baseUrl + formattedPath, jQueryCallback); |
223 | } | 215 | } |
224 | 216 | ||
225 | /** | 217 | /** |
226 | * Creates a request for this route that can later be called and converted into a | 218 | * Constructs a {@link Request} for a route that can later be executed and converted |
227 | * {@link JSONObject}. | 219 | * into a {@link JSONObject}. The resulting {@link Request} does not perform any |
220 | * session authentication with a {@link Token}. | ||
228 | * | 221 | * |
229 | * @param baseUrl | 222 | * @param baseUrl |
230 | * base (API's) URL | ||
231 | * @param filterProfanity | 223 | * @param filterProfanity |
232 | * whether to filter profanity. Akinator's website will automatically | ||
233 | * enable that if you choose an age below 16 | ||
234 | * @param parameters | 224 | * @param parameters |
235 | * parameters to pass to the route (parameters are specified in that | 225 | * |
236 | * Route's JavaDoc) | 226 | * @return a {@link Request}. |
237 | * @return a callable request | 227 | * |
238 | * @throws IOException | ||
239 | * @throws IllegalArgumentException | 228 | * @throws IllegalArgumentException |
240 | * if you have passed too little parameters | 229 | * if you have passed too little parameters. |
241 | */ | 230 | */ |
242 | public Request getRequest(String baseUrl, boolean filterProfanity, String... parameters) throws IOException { | 231 | @Nonnull |
232 | public Request getRequest(@Nonnull String baseUrl, boolean filterProfanity, @Nonnull String... parameters) { | ||
243 | return this.getRequest(baseUrl, filterProfanity, null, parameters); | 233 | return this.getRequest(baseUrl, filterProfanity, null, parameters); |
244 | } | 234 | } |
245 | 235 | ||
246 | /** | 236 | /** |
247 | * Sets the user-agent that will be used in requests for this route. If no user-agent | 237 | * Returns {@link Route}'s unformatted path. |
248 | * is specified, {@link AkiwrapperMetadata#DEFAULT_USER_AGENT} will be used. | ||
249 | * | 238 | * |
250 | * @param userAgent | 239 | * @return route's path. |
251 | * @return self, useful for chaining | ||
252 | */ | ||
253 | public Route setUserAgent(String userAgent) { | ||
254 | this.userAgent = userAgent; | ||
255 | |||
256 | return this; | ||
257 | } | ||
258 | |||
259 | /** | ||
260 | * @return route's path (unformatted) | ||
261 | */ | 240 | */ |
241 | @Nonnull | ||
262 | public String getPath() { | 242 | public String getPath() { |
263 | return this.path; | 243 | return this.path; |
264 | } | 244 | } |
265 | 245 | ||
266 | /** | 246 | /** |
267 | * @return minimal quantity of parameters you would have to pass to | 247 | * Returns the minimal quantity of parameters that must be passed to |
268 | * {@link #getRequest(String, boolean, String...)} | 248 | * {@link #getRequest(String, boolean, String...)} and |
249 | * {@link #getRequest(String, boolean, Token, String...)}. If the amount of passed | ||
250 | * parameters is lower than this number, an {@link IllegalArgumentException} is | ||
251 | * thrown. | ||
252 | * | ||
253 | * @return minimal quantity of parameters. | ||
269 | */ | 254 | */ |
270 | public int getParametersQuantity() { | 255 | public int getParametersQuantity() { |
271 | return this.parametersQuantity; | 256 | return this.parametersQuantity; |
272 | } | 257 | } |
273 | 258 | ||
274 | /** | 259 | /** |
275 | * @return user-agent for this route | 260 | * An executable request. |
276 | * @see #setUserAgent(String) | ||
277 | */ | ||
278 | public String getClientBuilder() { | ||
279 | return this.userAgent; | ||
280 | } | ||
281 | |||
282 | /** | ||
283 | * A callable request. | ||
284 | * | 261 | * |
285 | * @author Marko Zajc | 262 | * @author Marko Zajc |
286 | */ | 263 | */ |
287 | public static class Request { | 264 | public static class Request { |
288 | 265 | ||
289 | /** | 266 | private static final Logger LOG = LoggerFactory.getLogger(Route.Request.class); |
290 | * The connection timeout in milliseconds. Set this to something lower if you're | ||
291 | * going to send a lot of request to not-confirmed servers. Set this to {@code -1} to | ||
292 | * use {@link URLConnection}'s default timeout setting. <b>You usually don't need to | ||
293 | * alter this value</b> | ||
294 | */ | ||
295 | @SuppressFBWarnings({ | ||
296 | "MS_CANNOT_BE_FINAL", "MS_SHOULD_BE_FINAL" | ||
297 | }) | ||
298 | public static int connectionTimeout = 2500; // NOSONAR | ||
299 | 267 | ||
300 | URLConnection connection; | 268 | @Nonnull |
301 | private byte[] bytes = null; | 269 | private final String url; |
270 | @Nonnull | ||
302 | private final String jQueryCallback; | 271 | private final String jQueryCallback; |
303 | 272 | ||
304 | Request(URL url, String userAgent, String jQueryCallback) throws IOException { | 273 | Request(@Nonnull String url, @Nonnull String jQueryCallback) { |
305 | this.jQueryCallback = jQueryCallback; | 274 | this.jQueryCallback = jQueryCallback; |
306 | this.connection = url.openConnection(); | 275 | this.url = url; |
307 | if (connectionTimeout != -1) | ||
308 | this.connection.setConnectTimeout(connectionTimeout); | ||
309 | |||
310 | this.connection.setRequestProperty("User-Agent", userAgent); | ||
311 | } | ||
312 | |||
313 | /** | ||
314 | * Reads content of the request's URL into an array of bytes. | ||
315 | * | ||
316 | * @return content as a byte array | ||
317 | * @throws IOException | ||
318 | * @see String#String(byte[], String) | ||
319 | */ | ||
320 | public byte[] read() throws IOException { | ||
321 | if (this.bytes == null) { | ||
322 | byte[] newBytes = HTTPUtils.read(this.connection); | ||
323 | this.bytes = newBytes; | ||
324 | } | ||
325 | |||
326 | return this.bytes.clone(); | ||
327 | } | 276 | } |
328 | 277 | ||
329 | /** | 278 | /** |
330 | * Requests the server and returns the route's content as a {@link JSONObject}. | 279 | * Requests the server and returns the route's content as a {@link JSONObject}. |
331 | * | 280 | * |
332 | * @return route's content | 281 | * @return route's content |
333 | * @throws IOException | 282 | * |
334 | * @throws ServerUnavailableException | 283 | * @throws ServerUnavailableException |
335 | * in case the server has went down (very unlikely to ever happen) | 284 | * if the server has gone down |
285 | * @throws StatusException | ||
286 | * if the server returns an error response. | ||
336 | */ | 287 | */ |
337 | public JSONObject getJSON() throws IOException { | 288 | @Nonnull |
289 | public JSONObject getJSON() { | ||
338 | return getJSON(defaultRunChecks); | 290 | return getJSON(defaultRunChecks); |
339 | } | 291 | } |
340 | 292 | ||
@@ -343,34 +295,31 @@ public class Route { | |||
343 | * | 295 | * |
344 | * @param runChecks | 296 | * @param runChecks |
345 | * whether to run checks for error status codes. | 297 | * whether to run checks for error status codes. |
298 | * | ||
346 | * @return route's content | 299 | * @return route's content |
347 | * @throws IOException | 300 | * |
348 | * @throws ServerUnavailableException | 301 | * @throws ServerUnavailableException |
349 | * thrown if the server has gone down | 302 | * if the server has gone down. |
350 | * @throws StatusException | 303 | * @throws StatusException |
351 | * thrown if the server returns an error response | 304 | * if the server returns an error response. |
352 | * | 305 | * |
353 | */ | 306 | */ |
354 | public JSONObject getJSON(boolean runChecks) throws IOException { | 307 | @Nonnull |
355 | String response = new String(read(), StandardCharsets.UTF_8).replace(this.jQueryCallback, ""); | 308 | public JSONObject getJSON(boolean runChecks) { |
309 | String response = UNIREST.get(this.url).asString().getBody().replace(this.jQueryCallback, ""); | ||
356 | response = response.substring(1, response.length() - 1); | 310 | response = response.substring(1, response.length() - 1); |
357 | JSONObject result = new JSONObject(response); | 311 | LOG.trace("--> {}", this.url); |
312 | LOG.trace("<-- {}", response); | ||
313 | JSONObject result; | ||
314 | try { | ||
315 | result = new JSONObject(response); | ||
316 | } catch (JSONException e) { | ||
317 | LOG.error("Failed to parse JSON from the API server", e); | ||
318 | throw new StatusException(new StatusImpl("AW-KO - COULDN'T PARSE JSON")); | ||
319 | } | ||
358 | 320 | ||
359 | if (runChecks) | 321 | if (runChecks) |
360 | testResponse(result, new Server() { | 322 | testResponse(result); |
361 | |||
362 | @Override | ||
363 | public Language getLocalization() { | ||
364 | throw new UnsupportedOperationException(); // testResponse() does not need to know the language | ||
365 | } | ||
366 | |||
367 | @Override | ||
368 | public String getHost() { | ||
369 | return Request.this.connection.getURL().getHost() + ":" | ||
370 | + Request.this.connection.getURL().getPort(); | ||
371 | } | ||
372 | |||
373 | }); | ||
374 | 323 | ||
375 | return result; | 324 | return result; |
376 | } | 325 | } |
diff --git a/src/main/java/com/markozajc/akiwrapper/core/entities/AkiwrapperMetadata.java b/src/main/java/com/markozajc/akiwrapper/core/entities/AkiwrapperMetadata.java index bff5b7b..d9ab7f4 100644 --- a/src/main/java/com/markozajc/akiwrapper/core/entities/AkiwrapperMetadata.java +++ b/src/main/java/com/markozajc/akiwrapper/core/entities/AkiwrapperMetadata.java | |||
@@ -4,65 +4,75 @@ import javax.annotation.Nonnull; | |||
4 | import javax.annotation.Nullable; | 4 | import javax.annotation.Nullable; |
5 | 5 | ||
6 | import com.markozajc.akiwrapper.Akiwrapper; | 6 | import com.markozajc.akiwrapper.Akiwrapper; |
7 | import com.markozajc.akiwrapper.core.entities.Server.GuessType; | ||
7 | import com.markozajc.akiwrapper.core.entities.Server.Language; | 8 | import com.markozajc.akiwrapper.core.entities.Server.Language; |
8 | 9 | ||
9 | /** | 10 | /** |
10 | * A set of vital data used in API calls and such. | 11 | * A class holding configuration for an {@link Akiwrapper} instance. Note that |
12 | * {@link Language}, {@link GuessType}, and {@link Server} configuration are | ||
13 | * connected - {@link Language} and {@link GuessType} are used to find a suitable | ||
14 | * {@link Server}, but they will only be used if a {@link Server} is not manually | ||
15 | * set. It is not recommended to set the {@link Server} manually (unless for | ||
16 | * debugging purposes or as some kind of workaround where Akiwrapper's server finder | ||
17 | * fails) as Akiwrapper already does its best to find the most suitable one. | ||
11 | * | 18 | * |
12 | * @author Marko Zajc | 19 | * @author Marko Zajc |
13 | */ | 20 | */ |
14 | public abstract class AkiwrapperMetadata { | 21 | public abstract class AkiwrapperMetadata { |
15 | 22 | ||
16 | /** | 23 | /** |
17 | * The default name for new {@link Akiwrapper} instances. | 24 | * The default profanity filter preference for new {@link Akiwrapper} instances. |
18 | */ | ||
19 | public static final String DEFAULT_NAME = "website-desktop"; | ||
20 | |||
21 | /** | ||
22 | * The default user-agent for new {@link Akiwrapper} instances. | ||
23 | */ | ||
24 | public static final String DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) " | ||
25 | + "Chrome/66.0.3359.181 Safari/537.36"; | ||
26 | |||
27 | /** | ||
28 | * The default profanity filter for new {@link Akiwrapper} instances. | ||
29 | */ | 25 | */ |
30 | public static final boolean DEFAULT_FILTER_PROFANITY = false; | 26 | public static final boolean DEFAULT_FILTER_PROFANITY = false; |
31 | 27 | ||
32 | /** | 28 | /** |
33 | * The default localization language for new {@link Akiwrapper} instances. | 29 | * The default {@link Language} for new {@link Akiwrapper} instances. |
34 | */ | 30 | */ |
35 | @Nonnull | 31 | @Nonnull |
36 | public static final Language DEFAULT_LOCALIZATION = Language.ENGLISH; | 32 | public static final Language DEFAULT_LOCALIZATION = Language.ENGLISH; |
37 | 33 | ||
38 | /** | 34 | /** |
39 | * @return user's name, does not have any impact on gameplay | 35 | * The default {@link GuessType} for new {@link Akiwrapper} instances. |
40 | */ | ||
41 | @Nonnull | ||
42 | public abstract String getName(); | ||
43 | |||
44 | /** | ||
45 | * @return user-agent used in HTTP requests | ||
46 | */ | 36 | */ |
47 | @Nonnull | 37 | @Nonnull |
48 | public abstract String getUserAgent(); | 38 | public static final GuessType DEFAULT_GUESS_TYPE = GuessType.CHARACTER; |
49 | 39 | ||
50 | /** | 40 | /** |
51 | * @return the API server used for all requests. All API servers have equal data and | 41 | * Returns the {@link Server} that requests will be sent to. Might also return a |
52 | * endpoints but some might be down so you should never hard-code usage of a | 42 | * {@link ServerList} (which extends {@link Server}). |
53 | * specific API server | 43 | * |
44 | * @return server. | ||
54 | */ | 45 | */ |
55 | @Nullable | 46 | @Nullable |
56 | public abstract Server getServer(); | 47 | public abstract Server getServer(); |
57 | 48 | ||
58 | /** | 49 | /** |
59 | * @return whether to tell Akinator's API to filter out NSFW information | 50 | * Returns the profanity filter preference. Profanity filtering is done by Akinator |
51 | * and not by Akiwrapper. | ||
52 | * | ||
53 | * @return profanity filter preference. | ||
60 | */ | 54 | */ |
61 | public abstract boolean doesFilterProfanity(); | 55 | public abstract boolean doesFilterProfanity(); |
62 | 56 | ||
63 | /** | 57 | /** |
64 | * @return the language all elements will be in (eg. questions) | 58 | * Returns the {@link Language} preference. {@link Language} impacts what language |
59 | * {@link Question}s and {@link Guess}es are in.<br> | ||
60 | * {@link #getGuessType()} and {@link #getLanguage()} decide what {@link Server} will | ||
61 | * be used if it's not set manually. | ||
62 | * | ||
63 | * @return language preference. | ||
64 | */ | ||
65 | @Nonnull | ||
66 | public abstract Language getLanguage(); | ||
67 | |||
68 | /** | ||
69 | * Returns the {@link GuessType} preference. {@link GuessType} impacts what kind of | ||
70 | * subject {@link Question}s and {@link Guess}es are about.<br> | ||
71 | * {@link #getGuessType()} and {@link #getLanguage()} decide what {@link Server} will | ||
72 | * be used if it's not set manually. | ||
73 | * | ||
74 | * @return guess type preference. | ||
65 | */ | 75 | */ |
66 | @Nonnull | 76 | @Nonnull |
67 | public abstract Language getLocalization(); | 77 | public abstract GuessType getGuessType(); |
68 | } | 78 | } |
diff --git a/src/main/java/com/markozajc/akiwrapper/core/entities/Guess.java b/src/main/java/com/markozajc/akiwrapper/core/entities/Guess.java index d0dcc65..9cb886c 100644 --- a/src/main/java/com/markozajc/akiwrapper/core/entities/Guess.java +++ b/src/main/java/com/markozajc/akiwrapper/core/entities/Guess.java | |||
@@ -5,36 +5,55 @@ import java.net.URL; | |||
5 | import javax.annotation.Nonnull; | 5 | import javax.annotation.Nonnull; |
6 | import javax.annotation.Nullable; | 6 | import javax.annotation.Nullable; |
7 | 7 | ||
8 | import com.markozajc.akiwrapper.AkiwrapperBuilder; | ||
9 | import com.markozajc.akiwrapper.core.entities.Server.GuessType; | ||
10 | |||
8 | /** | 11 | /** |
9 | * A class used to represent Akinator's guess from the data acquired from your | 12 | * A representation of Akinator's guess. A guess may span different types of subject, |
10 | * answers. | 13 | * depending on what was set for the {@link GuessType} in the |
14 | * {@link AkiwrapperBuilder} (default is {@link GuessType#CHARACTER}). A guess | ||
15 | * consists of four parts - subject name, description (both localized), a URL to the | ||
16 | * image of the subject, and the probability that the guess is correct. Note that | ||
17 | * image URL and description are optional, and may be {@code null}. | ||
11 | * | 18 | * |
12 | * @author Marko Zajc | 19 | * @author Marko Zajc |
13 | */ | 20 | */ |
14 | public interface Guess extends Identifiable { | 21 | public interface Guess extends Identifiable { |
15 | 22 | ||
16 | /** | 23 | /** |
17 | * @return guessed characer's name | 24 | * Returns the name of the guessed subject. This is provided in the language that was |
25 | * specified using the {@link AkiwrapperBuilder}. | ||
26 | * | ||
27 | * @return guessed characer's name. | ||
18 | */ | 28 | */ |
19 | @Nonnull | 29 | @Nonnull |
20 | public String getName(); | 30 | String getName(); |
21 | 31 | ||
22 | /** | 32 | /** |
23 | * @return probability that this is the answer. 1 is the most sure, 0 is the least | 33 | * Returns the approximate probability that the answer is the one user has in mind |
24 | * sure | 34 | * (as a double). |
35 | * | ||
36 | * @return probability that this is the right answer. | ||
25 | */ | 37 | */ |
26 | public double getProbability(); | 38 | double getProbability(); |
27 | 39 | ||
28 | /** | 40 | /** |
29 | * @return description of this character | 41 | * Returns the description of this subject. As a description is optional and thus not |
42 | * always present, this may be {@code null}. It is provided in the language that was | ||
43 | * specified using the {@link AkiwrapperBuilder}. | ||
44 | * | ||
45 | * @return description of the guessed subject. | ||
30 | */ | 46 | */ |
31 | @Nullable | 47 | @Nullable |
32 | public String getDescription(); | 48 | String getDescription(); |
33 | 49 | ||
34 | /** | 50 | /** |
51 | * Returns the URL to an image of this subject. As an image of the subject is | ||
52 | * optional and thus not always present, this may be {@code null}. | ||
53 | * | ||
35 | * @return URL to picture or null if no picture is attached | 54 | * @return URL to picture or null if no picture is attached |
36 | */ | 55 | */ |
37 | @Nullable | 56 | @Nullable |
38 | public URL getImage(); | 57 | URL getImage(); |
39 | 58 | ||
40 | } | 59 | } |
diff --git a/src/main/java/com/markozajc/akiwrapper/core/entities/Identifiable.java b/src/main/java/com/markozajc/akiwrapper/core/entities/Identifiable.java index 5241da0..57a5bfb 100644 --- a/src/main/java/com/markozajc/akiwrapper/core/entities/Identifiable.java +++ b/src/main/java/com/markozajc/akiwrapper/core/entities/Identifiable.java | |||
@@ -4,8 +4,8 @@ import javax.annotation.Nonnegative; | |||
4 | import javax.annotation.Nonnull; | 4 | import javax.annotation.Nonnull; |
5 | 5 | ||
6 | /** | 6 | /** |
7 | * An interface used to represent an identifiable object (if the object has an | 7 | * A representation of an object with a numeric identifier. Some objects in the API |
8 | * appended ID set by Akinator's servers). | 8 | * have an ID appended to them. |
9 | * | 9 | * |
10 | * @author Marko Zajc | 10 | * @author Marko Zajc |
11 | */ | 11 | */ |
diff --git a/src/main/java/com/markozajc/akiwrapper/core/entities/Question.java b/src/main/java/com/markozajc/akiwrapper/core/entities/Question.java index b8988ad..55dafdd 100644 --- a/src/main/java/com/markozajc/akiwrapper/core/entities/Question.java +++ b/src/main/java/com/markozajc/akiwrapper/core/entities/Question.java | |||
@@ -4,37 +4,51 @@ import javax.annotation.Nonnegative; | |||
4 | import javax.annotation.Nonnull; | 4 | import javax.annotation.Nonnull; |
5 | 5 | ||
6 | import com.markozajc.akiwrapper.Akiwrapper.Answer; | 6 | import com.markozajc.akiwrapper.Akiwrapper.Answer; |
7 | import com.markozajc.akiwrapper.AkiwrapperBuilder; | ||
7 | 8 | ||
8 | /** | 9 | /** |
9 | * A class used to represent Akinator's question that can be answered with an | 10 | * A representation of Akinator's question that is to be answered with an |
10 | * {@link Answer}. | 11 | * {@link Answer}. Each {@link Question} object has a localized string question, a |
12 | * step number, gain, and progression. | ||
11 | * | 13 | * |
12 | * @author Marko Zajc | 14 | * @author Marko Zajc |
13 | */ | 15 | */ |
14 | public interface Question extends Identifiable { | 16 | public interface Question extends Identifiable { |
15 | 17 | ||
16 | /** | 18 | /** |
17 | * @return current completion percentage (as a double). Higher means closer to the | 19 | * Current completion percentage (as a double). Higher means that Akinator is closer |
18 | * answer | 20 | * to the correct answer. Not sure if that's the case, but I believe this can go down |
21 | * as well. | ||
22 | * | ||
23 | * @return completion percentage. | ||
19 | */ | 24 | */ |
20 | @Nonnegative | 25 | @Nonnegative |
21 | double getProgression(); | 26 | double getProgression(); |
22 | 27 | ||
23 | /** | 28 | /** |
24 | * @return current step (question number). This uses zero-based index, meaning the | 29 | * Returns the current step (question number). This uses zero-based index, meaning |
25 | * first question will be on step {@code 0} | 30 | * the first question will be on step {@code 0}. |
31 | * | ||
32 | * @return current step. | ||
26 | */ | 33 | */ |
27 | @Nonnegative | 34 | @Nonnegative |
28 | int getStep(); | 35 | int getStep(); |
29 | 36 | ||
30 | /** | 37 | /** |
31 | * @return gain from the last question | 38 | * Returns the gained accuracy from the last question (as a double). I'm not exactly |
39 | * sure what this does, but I'm pretty sure that it's meant to describe how well | ||
40 | * Akinator can pinpoint the answer after a question was with the answered question. | ||
41 | * | ||
42 | * @return accuracy gain. | ||
32 | */ | 43 | */ |
33 | @Nonnegative | 44 | @Nonnegative |
34 | double getGain(); | 45 | double getGain(); |
35 | 46 | ||
36 | /** | 47 | /** |
37 | * @return the actual question the user should answer to | 48 | * Returns the actual question that the user must answer. This is provided in the |
49 | * language that was specified using the {@link AkiwrapperBuilder}. | ||
50 | * | ||
51 | * @return question. | ||
38 | */ | 52 | */ |
39 | @Nonnull | 53 | @Nonnull |
40 | String getQuestion(); | 54 | String getQuestion(); |
diff --git a/src/main/java/com/markozajc/akiwrapper/core/entities/Server.java b/src/main/java/com/markozajc/akiwrapper/core/entities/Server.java index 4117f09..3c98f96 100644 --- a/src/main/java/com/markozajc/akiwrapper/core/entities/Server.java +++ b/src/main/java/com/markozajc/akiwrapper/core/entities/Server.java | |||
@@ -1,82 +1,135 @@ | |||
1 | package com.markozajc.akiwrapper.core.entities; | 1 | package com.markozajc.akiwrapper.core.entities; |
2 | 2 | ||
3 | import javax.annotation.Nonnull; | 3 | import javax.annotation.Nonnull; |
4 | import javax.annotation.Nullable; | ||
4 | 5 | ||
6 | import com.markozajc.akiwrapper.core.Route; | ||
7 | import com.markozajc.akiwrapper.core.exceptions.ServerNotFoundException; | ||
5 | import com.markozajc.akiwrapper.core.utils.Servers; | 8 | import com.markozajc.akiwrapper.core.utils.Servers; |
6 | 9 | ||
7 | /** | 10 | /** |
8 | * An interface representing an API server. | 11 | * A representation of an API server. All requests (except for |
12 | * {@link Route#NEW_SESSION} are passed to an such server. Each server has a | ||
13 | * predefined {@link Language} and {@link GuessType}. | ||
9 | * | 14 | * |
10 | * @author Marko Zajc | 15 | * @author Marko Zajc |
11 | */ | 16 | */ |
12 | public interface Server { | 17 | public interface Server { |
13 | 18 | ||
14 | /** | 19 | /** |
15 | * A localization language specific to a {@link Server} (or a {@link ServerGroup}). | 20 | * A language specific to a {@link Server}. |
16 | * | 21 | * |
17 | * @author Marko Zajc | 22 | * @author Marko Zajc |
18 | */ | 23 | */ |
19 | @SuppressWarnings("javadoc") | 24 | @SuppressWarnings("javadoc") |
20 | public enum Language { | 25 | public enum Language { |
21 | ARABIC, | ||
22 | CHINESE, | ||
23 | DUTCH, | ||
24 | ENGLISH, | ||
25 | FRENCH, | ||
26 | GERMAN, | ||
27 | HEBREW, | ||
28 | ITALIAN, | ||
29 | JAPANESE, | ||
30 | KOREAN, | ||
31 | MALAY, | ||
32 | POLISH, | ||
33 | PORTUGUESE, | ||
34 | RUSSIAN, | ||
35 | SPANISH, | ||
36 | TURKISH, | ||
37 | } | ||
38 | 26 | ||
39 | /** | 27 | ARABIC("ar"), |
40 | * @return the base (API's) URL for this server | 28 | CHINESE("cn"), |
41 | * @deprecated Changed for clarification. Use {@link #getApiUrl()} instead. | 29 | DUTCH("nl"), |
42 | */ | 30 | ENGLISH("en"), |
43 | @Deprecated | 31 | FRENCH("fr"), |
44 | @Nonnull | 32 | GERMAN("de"), |
45 | default String getBaseUrl() { | 33 | HEBREW("il"), |
46 | return getApiUrl(); | 34 | INDONESIAN("id"), |
35 | ITALIAN("it"), | ||
36 | JAPANESE("jp"), | ||
37 | KOREAN("kr"), | ||
38 | POLISH("pl"), | ||
39 | PORTUGUESE("pt"), | ||
40 | RUSSIAN("ru"), | ||
41 | SPANISH("es"), | ||
42 | TURKISH("tr"); | ||
43 | |||
44 | private final String id; | ||
45 | |||
46 | Language(String id) { | ||
47 | this.id = id; | ||
48 | } | ||
49 | |||
50 | public String getId() { | ||
51 | return this.id; | ||
52 | } | ||
53 | |||
54 | @Nullable | ||
55 | public static Language getById(@Nonnull String id) { | ||
56 | for (Language language : Language.values()) | ||
57 | if (id.equals(language.getId())) | ||
58 | return language; | ||
59 | return null; | ||
60 | } | ||
47 | } | 61 | } |
48 | 62 | ||
49 | /** | 63 | /** |
50 | * @return the base (API's) URL for this server | 64 | * Server's guess type (referred to as the "subject" in the API). Decides what kind |
65 | * of things server's guesses will represent. While the name might suggest that this | ||
66 | * affects only {@link Guess}es, it will also inevitably also impact | ||
67 | * {@link Question}s (it wouldn't make sense to ask the "Is it still alive" question | ||
68 | * for a place). <br> | ||
69 | * <b>Caution!</b> Not all {@link Language}s support all {@link GuessType}s. The | ||
70 | * standard ones seem to be {@link #ANIMAL}, {@link #CHARACTER}, and {@link #OBJECT}, | ||
71 | * but you might still face {@link ServerNotFoundException}s using them or other | ||
72 | * ones. | ||
73 | * | ||
74 | * @author Marko Zajc | ||
51 | */ | 75 | */ |
52 | @SuppressWarnings("null") | 76 | @SuppressWarnings("javadoc") |
53 | @Nonnull | 77 | public enum GuessType { |
54 | default String getApiUrl() { | 78 | |
55 | return String.format(Servers.BASE_URL_FORMAT, getHost()); | 79 | ANIMAL(14), |
80 | MOVIE_TV_SHOW(13), | ||
81 | PLACE(7), | ||
82 | CHARACTER(1), | ||
83 | OBJECT(2); | ||
84 | |||
85 | private final int id; | ||
86 | |||
87 | GuessType(int id) { | ||
88 | this.id = id; | ||
89 | } | ||
90 | |||
91 | public int getId() { | ||
92 | return this.id; | ||
93 | } | ||
94 | |||
95 | @Nullable | ||
96 | public static GuessType getById(int id) { | ||
97 | for (GuessType guessType : GuessType.values()) | ||
98 | if (id == guessType.getId()) | ||
99 | return guessType; | ||
100 | return null; | ||
101 | } | ||
102 | |||
56 | } | 103 | } |
57 | 104 | ||
58 | /** | 105 | /** |
59 | * @return the bare host for this server (in a {@code hostname:port} format) | 106 | * Server's host name. As the people behind Akinator tend to mix up their servers and |
107 | * the API in general, this should only fetch values from the server-listing endpoint | ||
108 | * (which is done in {@link Servers#getServers()}. The host is a valid URL, complete | ||
109 | * with the path to the endpoint.<br> | ||
110 | * Example: {@code https://srv3.akinator.com:9331/ws} | ||
111 | * | ||
112 | * @return server's host. | ||
60 | */ | 113 | */ |
61 | @Nonnull | 114 | @Nonnull |
62 | String getHost(); | 115 | String getUrl(); |
63 | 116 | ||
64 | /** | 117 | /** |
65 | * @return this server's localization language. The server will return localized | 118 | * Returns this {@link Server}'s {@link Language}. The server will return localized |
66 | * elements (eg. questions) depending on its localization language | 119 | * {@link Question}s and {@link Guess}es depending on its {@link Language}. |
120 | * | ||
121 | * @return server's language. | ||
67 | */ | 122 | */ |
68 | @Nonnull | 123 | @Nonnull |
69 | Language getLocalization(); | 124 | Language getLanguage(); |
70 | 125 | ||
71 | /** | 126 | /** |
72 | * Check if the current {@link Server} is still available. This is a shortcut for | 127 | * Returns this server's {@link GuessType}. The server will be returning guesses |
73 | * {@link Servers#isUp(Server)} | 128 | * based on that type (also referred to as the subject). |
74 | * | 129 | * |
75 | * @return true if that API server is available, false if not | 130 | * @return server's guess type. |
76 | * @see Servers#isUp(Server) | ||
77 | */ | 131 | */ |
78 | default boolean isUp() { | 132 | @Nonnull |
79 | return Servers.isUp(this); | 133 | GuessType getGuessType(); |
80 | } | ||
81 | 134 | ||
82 | } | 135 | } |
diff --git a/src/main/java/com/markozajc/akiwrapper/core/entities/ServerGroup.java b/src/main/java/com/markozajc/akiwrapper/core/entities/ServerGroup.java deleted file mode 100644 index 4368d81..0000000 --- a/src/main/java/com/markozajc/akiwrapper/core/entities/ServerGroup.java +++ /dev/null | |||
@@ -1,42 +0,0 @@ | |||
1 | package com.markozajc.akiwrapper.core.entities; | ||
2 | |||
3 | import java.util.List; | ||
4 | |||
5 | import javax.annotation.Nonnull; | ||
6 | import javax.annotation.Nullable; | ||
7 | |||
8 | import com.markozajc.akiwrapper.core.entities.Server.Language; | ||
9 | import com.markozajc.akiwrapper.core.utils.Servers; | ||
10 | |||
11 | /** | ||
12 | * An interface representing a group of API servers. Servers are (usually) grouped by | ||
13 | * their assigned language. | ||
14 | * | ||
15 | * @author Marko Zajc | ||
16 | */ | ||
17 | public interface ServerGroup { | ||
18 | |||
19 | /** | ||
20 | * @return current language of this server group | ||
21 | */ | ||
22 | @Nonnull | ||
23 | Language getLocalization(); | ||
24 | |||
25 | /** | ||
26 | * @return an unmodifiable list of servers of this {@link ServerGroup} | ||
27 | */ | ||
28 | @Nonnull | ||
29 | List<Server> getServers(); | ||
30 | |||
31 | /** | ||
32 | * @return the first available server of this {@link ServerGroup}. The chances of | ||
33 | * this returning {@code null} (aka all servers of this group are down) | ||
34 | * depend on this {@link ServerGroup}'s size. You should choose calling this | ||
35 | * method over getting a server manually with {@link #getServers()} | ||
36 | */ | ||
37 | @Nullable | ||
38 | default Server getFirstAvailableServer() { | ||
39 | return getServers().stream().filter(Servers::isUp).findFirst().orElse(null); | ||
40 | } | ||
41 | |||
42 | } | ||
diff --git a/src/main/java/com/markozajc/akiwrapper/core/entities/ServerList.java b/src/main/java/com/markozajc/akiwrapper/core/entities/ServerList.java new file mode 100644 index 0000000..72006aa --- /dev/null +++ b/src/main/java/com/markozajc/akiwrapper/core/entities/ServerList.java | |||
@@ -0,0 +1,72 @@ | |||
1 | package com.markozajc.akiwrapper.core.entities; | ||
2 | |||
3 | import java.sql.ResultSet; | ||
4 | import java.util.List; | ||
5 | import java.util.regex.Matcher; | ||
6 | |||
7 | import javax.annotation.Nonnull; | ||
8 | |||
9 | import com.markozajc.akiwrapper.core.exceptions.ServerUnavailableException; | ||
10 | |||
11 | /** | ||
12 | * A representation of multiple {@link Server}s at once. While a single | ||
13 | * {@link Server} instance is <i>usually</i> enough, it might go down. Akiwrapper | ||
14 | * will in that case automatically find the next available one in a given | ||
15 | * {@link ServerList}, until it hits into one that is available. If none are | ||
16 | * available, it will fail with a {@link ServerUnavailableException}.<br> | ||
17 | * Note: being available means succeeding to create a new session. Failure can mean | ||
18 | * that either: | ||
19 | * <ul> | ||
20 | * <li>the server is down, which is very unlikely, but is the best case scenario | ||
21 | * since no code needs to be fixed, | ||
22 | * <li>or that Akinator has changed something and Akiwrapper doesn't yet support the | ||
23 | * change, causing failure on a new session. | ||
24 | * </ul> | ||
25 | * The {@link ServerList} acts similarly to a {@link Matcher} or a {@link ResultSet} | ||
26 | * - you can use it the same way as you would a regular {@link Server}, and if it is | ||
27 | * down or does not work, you can call {@link #next()} to seamlessly switch to the | ||
28 | * next instance. | ||
29 | * | ||
30 | * @author Marko Zajc | ||
31 | */ | ||
32 | public interface ServerList extends Server { | ||
33 | |||
34 | /** | ||
35 | * Iterates to the next {@link Server} in the queue. Returns of all methods will be | ||
36 | * replaced with those of the next server. The return value of this method indicates | ||
37 | * success - {@code false} means that there are no more servers in the queue (the | ||
38 | * server is not changed), {@code true} means that the server has been changed. | ||
39 | * | ||
40 | * @return success indicator. | ||
41 | */ | ||
42 | boolean next(); | ||
43 | |||
44 | /** | ||
45 | * Returns the remaining {@link Server} in the queue plus the current {@link Server}. | ||
46 | * Note that calling {@link #next()} removes the cycles to the next server and | ||
47 | * removes the current one, meaning that it will not be included in this list. | ||
48 | * | ||
49 | * @return all servers. | ||
50 | */ | ||
51 | @Nonnull | ||
52 | List<Server> getServers(); | ||
53 | |||
54 | /** | ||
55 | * Returns the amount of remaining {@link Server}s in the queue. | ||
56 | * | ||
57 | * @return amount of remaining servers. | ||
58 | */ | ||
59 | int getRemainingSize(); | ||
60 | |||
61 | /** | ||
62 | * Checks whether or not this {@link ServerList} has another {@link Server} to | ||
63 | * iterate to. This performs the same check as {@link #next()}, but doesn't change | ||
64 | * the state and doesn't actually iterate to the next server. | ||
65 | * | ||
66 | * @return whether there is another server to iterate to. | ||
67 | */ | ||
68 | default boolean hasNext() { | ||
69 | return getRemainingSize() > 0; | ||
70 | } | ||
71 | |||
72 | } | ||
diff --git a/src/main/java/com/markozajc/akiwrapper/core/entities/Status.java b/src/main/java/com/markozajc/akiwrapper/core/entities/Status.java index 3b9ad95..16cae2b 100644 --- a/src/main/java/com/markozajc/akiwrapper/core/entities/Status.java +++ b/src/main/java/com/markozajc/akiwrapper/core/entities/Status.java | |||
@@ -1,40 +1,53 @@ | |||
1 | package com.markozajc.akiwrapper.core.entities; | 1 | package com.markozajc.akiwrapper.core.entities; |
2 | 2 | ||
3 | import java.io.Serializable; | ||
4 | |||
5 | import javax.annotation.Nullable; | ||
6 | |||
3 | /** | 7 | /** |
4 | * An interface used to represent API call's completion status. | 8 | * An interface used to represent API call's completion status. |
5 | * | 9 | * |
6 | * @author Marko Zajc | 10 | * @author Marko Zajc |
7 | */ | 11 | */ |
8 | public interface Status { | 12 | public interface Status extends Serializable { |
9 | 13 | ||
10 | /** | 14 | /** |
11 | * Indicates API call status level | 15 | * Indicates the severity of a response from the API server. |
12 | */ | 16 | */ |
13 | public enum Level { | 17 | public enum Level { |
14 | /** | ||
15 | * Everything is OK, you may continue normally. | ||
16 | */ | ||
17 | OK("OK"), | ||
18 | 18 | ||
19 | /** | 19 | /** |
20 | * The majority call has completed but something minor might have failed/not | 20 | * Everything is OK, you may continue normally. |
21 | * completed. | 21 | */ |
22 | */ | 22 | OK("OK"), |
23 | WARNING("WARN"), | ||
24 | 23 | ||
25 | /** | 24 | /** |
26 | * The call has not completed due to an error | 25 | * The action has completed, but something minor might have failed/not completed. |
27 | */ | 26 | */ |
28 | ERROR("KO"), | 27 | WARNING("WARN"), |
29 | 28 | ||
30 | /** | 29 | /** |
31 | * Unknown status (should not ever occur under normal circumstances) | 30 | * The action has not completed due to an error. |
32 | */ | 31 | */ |
33 | UNKNOWN(""); | 32 | ERROR("KO"), |
33 | |||
34 | /** | ||
35 | * The action might have completed completed, but an error has occurred on | ||
36 | * Akiwrapper's side. This status is never actually returned by the API, but is made | ||
37 | * up by Akiwrapper internally to indicate some errors caused by invalid or | ||
38 | * unexpected API responses. | ||
39 | */ | ||
40 | AKIWRAPPER_ERROR("AW-KO"), | ||
41 | |||
42 | /** | ||
43 | * Unknown status (should not ever occur under normal circumstances), indicates that | ||
44 | * the status level doesn't match any of the known ones. | ||
45 | */ | ||
46 | UNKNOWN(""); | ||
34 | 47 | ||
35 | private String name; | 48 | private String name; |
36 | 49 | ||
37 | private Level(String name) { | 50 | Level(String name) { |
38 | this.name = name; | 51 | this.name = name; |
39 | } | 52 | } |
40 | 53 | ||
@@ -49,17 +62,20 @@ public interface Status { | |||
49 | } | 62 | } |
50 | 63 | ||
51 | /** | 64 | /** |
52 | * Returns error level | 65 | * Returns the level of this status. Status level indicates severity of the status. |
53 | * | 66 | * |
54 | * @return status level | 67 | * @return status level |
55 | */ | 68 | */ |
56 | Level getLevel(); | 69 | Level getLevel(); |
57 | 70 | ||
58 | /** | 71 | /** |
59 | * Returns error reason | 72 | * Returns the status reason or {@code null} if it was not specified. Note that the |
73 | * status reason is usually pretty cryptic and won't mean much to regular users or | ||
74 | * anyone not experienced with the Akinator API. | ||
60 | * | 75 | * |
61 | * @return error reason or null if level is OK | 76 | * @return status reason |
62 | */ | 77 | */ |
78 | @Nullable | ||
63 | String getReason(); | 79 | String getReason(); |
64 | 80 | ||
65 | } | 81 | } |
diff --git a/src/main/java/com/markozajc/akiwrapper/core/entities/impl/immutable/ApiKey.java b/src/main/java/com/markozajc/akiwrapper/core/entities/impl/immutable/ApiKey.java new file mode 100644 index 0000000..f513cfa --- /dev/null +++ b/src/main/java/com/markozajc/akiwrapper/core/entities/impl/immutable/ApiKey.java | |||
@@ -0,0 +1,75 @@ | |||
1 | package com.markozajc.akiwrapper.core.entities.impl.immutable; | ||
2 | |||
3 | import java.io.UnsupportedEncodingException; | ||
4 | import java.net.URLEncoder; | ||
5 | import java.util.regex.Matcher; | ||
6 | import java.util.regex.Pattern; | ||
7 | |||
8 | import javax.annotation.Nonnull; | ||
9 | |||
10 | import com.markozajc.akiwrapper.core.Route; | ||
11 | import com.markozajc.akiwrapper.core.exceptions.StatusException; | ||
12 | |||
13 | /** | ||
14 | * A class defining the session key that has to be passed to the | ||
15 | * {@link Route#NEW_SESSION} endpoint. It is scraped from the website as it is | ||
16 | * single-use and triggers a KO if reused. | ||
17 | * | ||
18 | * @author Marko Zajc | ||
19 | * | ||
20 | */ | ||
21 | public class ApiKey { | ||
22 | |||
23 | private static final Pattern API_KEY_PATTERN = | ||
24 | Pattern.compile("var uid_ext_session = '(.*)'\\;\\n.*var frontaddr = '(.*)'\\;"); | ||
25 | |||
26 | private static final String FORMAT = "frontaddr=%s&uid_ext_session=%s"; | ||
27 | |||
28 | @Nonnull | ||
29 | private final String sessionUid; | ||
30 | @Nonnull | ||
31 | private final String frontAddress; | ||
32 | |||
33 | ApiKey(@Nonnull String sessionUid, @Nonnull String frontAddress) { | ||
34 | this.sessionUid = sessionUid; | ||
35 | this.frontAddress = frontAddress; | ||
36 | } | ||
37 | |||
38 | /** | ||
39 | * Compiles this {@link ApiKey} into querystring that can be appended to an endpoint. | ||
40 | * | ||
41 | * @return compiled querystring. | ||
42 | */ | ||
43 | @SuppressWarnings("null") | ||
44 | @Nonnull | ||
45 | public String compile() { | ||
46 | try { | ||
47 | return String.format(FORMAT, URLEncoder.encode(this.frontAddress, "UTF-8"), this.sessionUid); | ||
48 | } catch (UnsupportedEncodingException e) { | ||
49 | throw new RuntimeException(e); // never throws, UTF-8 is always supported | ||
50 | } | ||
51 | } | ||
52 | |||
53 | /** | ||
54 | * Finds and scrapes the API key from Akinator's website and constructs an | ||
55 | * {@link ApiKey} instance from it. <br> | ||
56 | * <b>Caution!</b>Each {@link ApiKey} is single-use! Reusing it will trigger a | ||
57 | * {@link StatusException} from the API server. | ||
58 | * | ||
59 | * @return an {@link ApiKey}. | ||
60 | * | ||
61 | * @throws IllegalStateException | ||
62 | * if the API key can't be scraped | ||
63 | */ | ||
64 | @SuppressWarnings("null") | ||
65 | public static ApiKey accquireApiKey() { | ||
66 | Matcher matcher = | ||
67 | API_KEY_PATTERN.matcher(Route.UNIREST.get(Route.BASE_AKINATOR_URL + "/game").asString().getBody()); | ||
68 | if (!matcher.find()) | ||
69 | throw new IllegalStateException("Couldn't find the API key! Please consider opening" + | ||
70 | "a new ticket at https://github.com/markozajc/Akiwrapper/issues."); | ||
71 | |||
72 | return new ApiKey(matcher.group(1), matcher.group(2)); | ||
73 | } | ||
74 | |||
75 | } | ||
diff --git a/src/main/java/com/markozajc/akiwrapper/core/entities/impl/immutable/GuessImpl.java b/src/main/java/com/markozajc/akiwrapper/core/entities/impl/immutable/GuessImpl.java index 87a7d5d..e5c9420 100644 --- a/src/main/java/com/markozajc/akiwrapper/core/entities/impl/immutable/GuessImpl.java +++ b/src/main/java/com/markozajc/akiwrapper/core/entities/impl/immutable/GuessImpl.java | |||
@@ -32,22 +32,6 @@ public class GuessImpl implements Guess { | |||
32 | @Nonnegative | 32 | @Nonnegative |
33 | private final double probability; | 33 | private final double probability; |
34 | 34 | ||
35 | @Nullable | ||
36 | private static URL getImage(@Nonnull JSONObject json) { | ||
37 | try { | ||
38 | return json.getString("picture_path").equals("none.jpg") ? null | ||
39 | : new URL(json.getString("absolute_picture_path")); | ||
40 | } catch (MalformedURLException e) { | ||
41 | return null; | ||
42 | } | ||
43 | } | ||
44 | |||
45 | @Nullable | ||
46 | private static String getDescription(@Nonnull JSONObject json) { | ||
47 | String desc = json.getString("description"); | ||
48 | return desc.equals("-") ? null : desc; | ||
49 | } | ||
50 | |||
51 | /** | 35 | /** |
52 | * Creates a new {@link GuessImpl} instance from raw parameters. | 36 | * Creates a new {@link GuessImpl} instance from raw parameters. |
53 | * | 37 | * |
@@ -58,7 +42,7 @@ public class GuessImpl implements Guess { | |||
58 | * @param probability | 42 | * @param probability |
59 | */ | 43 | */ |
60 | public GuessImpl(@Nonnull String id, @Nonnull String name, @Nullable String description, @Nullable URL image, | 44 | public GuessImpl(@Nonnull String id, @Nonnull String name, @Nullable String description, @Nullable URL image, |
61 | @Nonnegative double probability) { | 45 | @Nonnegative double probability) { |
62 | this.id = id; | 46 | this.id = id; |
63 | this.name = name; | 47 | this.name = name; |
64 | this.description = description; | 48 | this.description = description; |
@@ -70,14 +54,30 @@ public class GuessImpl implements Guess { | |||
70 | * Creates a new {@link GuessImpl} instance. | 54 | * Creates a new {@link GuessImpl} instance. |
71 | * | 55 | * |
72 | * @param json | 56 | * @param json |
73 | * JSON parameters to use (acquired with {@link Route#LIST} > | 57 | * JSON parameters to use (acquired with {@link Route#LIST} > |
74 | * {@link JSONArray} elements > {@link JSONObject} (an index) > | 58 | * {@link JSONArray} elements > {@link JSONObject} (an index) > |
75 | * {@link JSONObject} element) | 59 | * {@link JSONObject} element) |
76 | */ | 60 | */ |
77 | @SuppressWarnings("null") | 61 | @SuppressWarnings("null") |
78 | public GuessImpl(@Nonnull JSONObject json) { | 62 | public GuessImpl(@Nonnull JSONObject json) { |
79 | this(json.getString("id"), json.getString("name"), getDescription(json), getImage(json), | 63 | this(json.getString("id"), json.getString("name"), getDescription(json), getImage(json), |
80 | JSONUtils.getDouble(json, "proba").doubleValue()); | 64 | JSONUtils.getDouble(json, "proba").get().doubleValue()); |
65 | } | ||
66 | |||
67 | @Nullable | ||
68 | private static String getDescription(@Nonnull JSONObject json) { | ||
69 | String desc = json.getString("description"); | ||
70 | return "-".equals(desc) ? null : desc; | ||
71 | } | ||
72 | |||
73 | @Nullable | ||
74 | private static URL getImage(@Nonnull JSONObject json) { | ||
75 | try { | ||
76 | return "none.jpg".equals(json.getString("picture_path")) ? null | ||
77 | : new URL(json.getString("absolute_picture_path")); | ||
78 | } catch (MalformedURLException e) { | ||
79 | return null; | ||
80 | } | ||
81 | } | 81 | } |
82 | 82 | ||
83 | @Override | 83 | @Override |
diff --git a/src/main/java/com/markozajc/akiwrapper/core/entities/impl/immutable/ImmutableAkiwrapperMetadata.java b/src/main/java/com/markozajc/akiwrapper/core/entities/impl/immutable/ImmutableAkiwrapperMetadata.java index 24fdb3e..cec7d3a 100644 --- a/src/main/java/com/markozajc/akiwrapper/core/entities/impl/immutable/ImmutableAkiwrapperMetadata.java +++ b/src/main/java/com/markozajc/akiwrapper/core/entities/impl/immutable/ImmutableAkiwrapperMetadata.java | |||
@@ -1,10 +1,13 @@ | |||
1 | package com.markozajc.akiwrapper.core.entities.impl.immutable; | 1 | package com.markozajc.akiwrapper.core.entities.impl.immutable; |
2 | 2 | ||
3 | import javax.annotation.Nonnull; | 3 | import javax.annotation.Nonnull; |
4 | import javax.annotation.Nullable; | ||
4 | 5 | ||
5 | import com.markozajc.akiwrapper.core.entities.AkiwrapperMetadata; | 6 | import com.markozajc.akiwrapper.core.entities.AkiwrapperMetadata; |
7 | import com.markozajc.akiwrapper.core.entities.Guess; | ||
6 | import com.markozajc.akiwrapper.core.entities.Question; | 8 | import com.markozajc.akiwrapper.core.entities.Question; |
7 | import com.markozajc.akiwrapper.core.entities.Server; | 9 | import com.markozajc.akiwrapper.core.entities.Server; |
10 | import com.markozajc.akiwrapper.core.entities.Server.GuessType; | ||
8 | import com.markozajc.akiwrapper.core.entities.Server.Language; | 11 | import com.markozajc.akiwrapper.core.entities.Server.Language; |
9 | 12 | ||
10 | /** | 13 | /** |
@@ -14,49 +17,32 @@ import com.markozajc.akiwrapper.core.entities.Server.Language; | |||
14 | */ | 17 | */ |
15 | public abstract class ImmutableAkiwrapperMetadata extends AkiwrapperMetadata { | 18 | public abstract class ImmutableAkiwrapperMetadata extends AkiwrapperMetadata { |
16 | 19 | ||
17 | @Nonnull | 20 | @Nullable |
18 | protected final String name; | ||
19 | @Nonnull | ||
20 | protected final String userAgent; | ||
21 | @Nonnull | ||
22 | protected final Server server; | 21 | protected final Server server; |
23 | protected final boolean filterProfanity; | 22 | protected final boolean filterProfanity; |
24 | @Nonnull | 23 | @Nonnull |
25 | protected final Language localization; | 24 | protected final Language localization; |
25 | @Nonnull | ||
26 | protected final GuessType guessType; | ||
26 | 27 | ||
27 | /** | 28 | /** |
28 | * Creates a new {@link ImmutableAkiwrapperMetadata} instance. | 29 | * Creates a new {@link ImmutableAkiwrapperMetadata} instance. |
29 | * | 30 | * |
30 | * @param server | 31 | * @param server |
31 | * the API server to use | 32 | * API server that the requests will be sent to. |
32 | * @param name | ||
33 | * player's name (won't have any huge impact but is still passed to the | ||
34 | * Akinator API for convenience) | ||
35 | * @param userAgent | ||
36 | * the user-agent to use | ||
37 | * @param filterProfanity | 33 | * @param filterProfanity |
38 | * whether to filter out all profanity elements | 34 | * whether to filter out NSFW {@link Question}s and {@link Guess}es. |
39 | * @param localization | 35 | * @param language |
40 | * the localization language that will be passed to the API server. This | 36 | * {@link Language} of {@link Question}s. |
41 | * affects textual elements such as {@link Question}-s | 37 | * @param guessType |
38 | * {@link GuessType} of {@link Guess}es. | ||
42 | */ | 39 | */ |
43 | public ImmutableAkiwrapperMetadata(@Nonnull String name, @Nonnull String userAgent, @Nonnull Server server, | 40 | public ImmutableAkiwrapperMetadata(@Nullable Server server, boolean filterProfanity, @Nonnull Language language, |
44 | boolean filterProfanity, @Nonnull Language localization) { | 41 | @Nonnull GuessType guessType) { |
45 | this.name = name; | ||
46 | this.userAgent = userAgent; | ||
47 | this.server = server; | 42 | this.server = server; |
48 | this.filterProfanity = filterProfanity; | 43 | this.filterProfanity = filterProfanity; |
49 | this.localization = localization; | 44 | this.localization = language; |
50 | } | 45 | this.guessType = guessType; |
51 | |||
52 | @Override | ||
53 | public String getName() { | ||
54 | return this.name; | ||
55 | } | ||
56 | |||
57 | @Override | ||
58 | public String getUserAgent() { | ||
59 | return this.userAgent; | ||
60 | } | 46 | } |
61 | 47 | ||
62 | @Override | 48 | @Override |
@@ -70,8 +56,13 @@ public abstract class ImmutableAkiwrapperMetadata extends AkiwrapperMetadata { | |||
70 | } | 56 | } |
71 | 57 | ||
72 | @Override | 58 | @Override |
73 | public Language getLocalization() { | 59 | public Language getLanguage() { |
74 | return this.localization; | 60 | return this.localization; |
75 | } | 61 | } |
76 | 62 | ||
63 | @Override | ||
64 | public GuessType getGuessType() { | ||
65 | return this.guessType; | ||
66 | } | ||
67 | |||
77 | } | 68 | } |
diff --git a/src/main/java/com/markozajc/akiwrapper/core/entities/impl/immutable/QuestionImpl.java b/src/main/java/com/markozajc/akiwrapper/core/entities/impl/immutable/QuestionImpl.java index 7cbb99f..1787fd2 100644 --- a/src/main/java/com/markozajc/akiwrapper/core/entities/impl/immutable/QuestionImpl.java +++ b/src/main/java/com/markozajc/akiwrapper/core/entities/impl/immutable/QuestionImpl.java | |||
@@ -5,7 +5,6 @@ import javax.annotation.Nonnull; | |||
5 | 5 | ||
6 | import org.json.JSONObject; | 6 | import org.json.JSONObject; |
7 | 7 | ||
8 | import com.markozajc.akiwrapper.core.Route; | ||
9 | import com.markozajc.akiwrapper.core.entities.Question; | 8 | import com.markozajc.akiwrapper.core.entities.Question; |
10 | import com.markozajc.akiwrapper.core.entities.Status; | 9 | import com.markozajc.akiwrapper.core.entities.Status; |
11 | import com.markozajc.akiwrapper.core.entities.Status.Level; | 10 | import com.markozajc.akiwrapper.core.entities.Status.Level; |
@@ -31,7 +30,7 @@ public class QuestionImpl implements Question { | |||
31 | private final double progression; | 30 | private final double progression; |
32 | 31 | ||
33 | /** | 32 | /** |
34 | * Creates a new {@link QuestionImpl} instance from raw parameters. | 33 | * Constructs a new {@link QuestionImpl} instance from raw parameters. |
35 | * | 34 | * |
36 | * @param id | 35 | * @param id |
37 | * @param question | 36 | * @param question |
@@ -39,14 +38,13 @@ public class QuestionImpl implements Question { | |||
39 | * @param gain | 38 | * @param gain |
40 | * @param progression | 39 | * @param progression |
41 | * @param status | 40 | * @param status |
41 | * | ||
42 | * @throws MissingQuestionException | 42 | * @throws MissingQuestionException |
43 | * if the message is missing (no more messages left to answer, get the | 43 | * if there are no more questions left. |
44 | * final guesses) | ||
45 | */ | 44 | */ |
46 | public QuestionImpl(@Nonnull String id, @Nonnull String question, @Nonnegative int step, @Nonnegative double gain, | 45 | public QuestionImpl(@Nonnull String id, @Nonnull String question, @Nonnegative int step, @Nonnegative double gain, |
47 | @Nonnegative double progression, @Nonnull Status status) { | 46 | @Nonnegative double progression, @Nonnull Status status) { |
48 | if (status.getLevel().equals(Level.WARNING) && status.getReason().equalsIgnoreCase("no question")) | 47 | checkMissingQuestion(status); |
49 | throw new MissingQuestionException(); | ||
50 | 48 | ||
51 | this.id = id; | 49 | this.id = id; |
52 | this.question = question; | 50 | this.question = question; |
@@ -56,27 +54,27 @@ public class QuestionImpl implements Question { | |||
56 | } | 54 | } |
57 | 55 | ||
58 | /** | 56 | /** |
59 | * Creates a new {@link QuestionImpl} instance. | 57 | * Constructs a new {@link QuestionImpl} instance from a {@link JSONObject}. |
60 | * | 58 | * |
61 | * @param json | 59 | * @param json |
62 | * JSON parameters to use (acquired with {@link Route#ANSWER} or | ||
63 | * {@link Route#NEW_SESSION} > {@link JSONObject} parameters) | ||
64 | * @param status | 60 | * @param status |
65 | * call completion status | 61 | * |
66 | * @throws MissingQuestionException | 62 | * @throws MissingQuestionException |
67 | * if the message is missing (no more messages left to answer, get the | 63 | * if there are no more questions left. |
68 | * final guesses) | ||
69 | */ | 64 | */ |
70 | @SuppressWarnings("null") | 65 | @SuppressWarnings("null") |
71 | public QuestionImpl(@Nonnull JSONObject json, @Nonnull Status status) { | 66 | public QuestionImpl(@Nonnull JSONObject json, @Nonnull Status status) { |
72 | if (status.getLevel().equals(Level.WARNING) && status.getReason().toLowerCase().equalsIgnoreCase("no question")) | 67 | checkMissingQuestion(status); |
73 | throw new MissingQuestionException(); | ||
74 | |||
75 | this.id = json.getString("questionid"); | 68 | this.id = json.getString("questionid"); |
76 | this.question = json.getString("question"); | 69 | this.question = json.getString("question"); |
77 | this.step = JSONUtils.getInteger(json, "step").intValue(); | 70 | this.step = JSONUtils.getInteger(json, "step").get().intValue(); |
78 | this.gain = JSONUtils.getDouble(json, "infogain").doubleValue(); | 71 | this.gain = JSONUtils.getDouble(json, "infogain").get().doubleValue(); |
79 | this.progression = JSONUtils.getDouble(json, "progression").doubleValue(); | 72 | this.progression = JSONUtils.getDouble(json, "progression").get().doubleValue(); |
73 | } | ||
74 | |||
75 | private static void checkMissingQuestion(@Nonnull Status status) { | ||
76 | if (status.getLevel() == Level.WARNING && "no question".equalsIgnoreCase(status.getReason())) | ||
77 | throw new MissingQuestionException(); | ||
80 | } | 78 | } |
81 | 79 | ||
82 | @Override | 80 | @Override |
diff --git a/src/main/java/com/markozajc/akiwrapper/core/entities/impl/immutable/ServerGroupImpl.java b/src/main/java/com/markozajc/akiwrapper/core/entities/impl/immutable/ServerGroupImpl.java deleted file mode 100644 index de346db..0000000 --- a/src/main/java/com/markozajc/akiwrapper/core/entities/impl/immutable/ServerGroupImpl.java +++ /dev/null | |||
@@ -1,73 +0,0 @@ | |||
1 | package com.markozajc.akiwrapper.core.entities.impl.immutable; | ||
2 | |||
3 | import java.util.Arrays; | ||
4 | import java.util.Collections; | ||
5 | import java.util.List; | ||
6 | |||
7 | import javax.annotation.Nonnull; | ||
8 | |||
9 | import com.markozajc.akiwrapper.core.entities.Server; | ||
10 | import com.markozajc.akiwrapper.core.entities.Server.Language; | ||
11 | import com.markozajc.akiwrapper.core.entities.ServerGroup; | ||
12 | |||
13 | /** | ||
14 | * An implementation of {@link ServerGroup}. | ||
15 | * | ||
16 | * @author Marko Zajc | ||
17 | */ | ||
18 | public class ServerGroupImpl implements ServerGroup { | ||
19 | |||
20 | @Nonnull | ||
21 | private final Language localization; | ||
22 | @Nonnull | ||
23 | private final List<Server> servers; | ||
24 | |||
25 | /** | ||
26 | * Creates a new {@link ServerGroupImpl} instance. | ||
27 | * | ||
28 | * @param localization | ||
29 | * language of this {@link ServerGroupImpl} | ||
30 | * @param servers | ||
31 | * servers of this {@link ServerGroupImpl} | ||
32 | * @throws IllegalArgumentException | ||
33 | * in case one or more of the given servers from {@code servers} do not | ||
34 | * have the same localization as {@code localization}. | ||
35 | */ | ||
36 | @SuppressWarnings("null") | ||
37 | public ServerGroupImpl(@Nonnull Language localization, @Nonnull List<Server> servers) { | ||
38 | if (servers.stream().anyMatch(s -> !s.getLocalization().equals(localization))) | ||
39 | throw new IllegalArgumentException( | ||
40 | "One or more servers do not have the same localization as this ServerGroup (" + localization.toString() | ||
41 | + "!"); | ||
42 | |||
43 | this.localization = localization; | ||
44 | this.servers = Collections.unmodifiableList(servers); | ||
45 | } | ||
46 | |||
47 | /** | ||
48 | * Creates a new {@link ServerGroupImpl} instance. | ||
49 | * | ||
50 | * @param localization | ||
51 | * language of this {@link ServerGroupImpl} | ||
52 | * @param servers | ||
53 | * servers of this {@link ServerGroupImpl} (as varargs) | ||
54 | * @throws IllegalArgumentException | ||
55 | * in case one or more of the given servers from {@code servers} do not | ||
56 | * have the same localization as {@code localization}. | ||
57 | */ | ||
58 | @SuppressWarnings("null") | ||
59 | public ServerGroupImpl(@Nonnull Language localization, @Nonnull Server... servers) { | ||
60 | this(localization, Arrays.asList(servers)); | ||
61 | } | ||
62 | |||
63 | @Override | ||
64 | public Language getLocalization() { | ||
65 | return this.localization; | ||
66 | } | ||
67 | |||
68 | @Override | ||
69 | public List<Server> getServers() { | ||
70 | return this.servers; | ||
71 | } | ||
72 | |||
73 | } | ||
diff --git a/src/main/java/com/markozajc/akiwrapper/core/entities/impl/immutable/ServerImpl.java b/src/main/java/com/markozajc/akiwrapper/core/entities/impl/immutable/ServerImpl.java index 81d9d57..35a449b 100644 --- a/src/main/java/com/markozajc/akiwrapper/core/entities/impl/immutable/ServerImpl.java +++ b/src/main/java/com/markozajc/akiwrapper/core/entities/impl/immutable/ServerImpl.java | |||
@@ -1,7 +1,11 @@ | |||
1 | package com.markozajc.akiwrapper.core.entities.impl.immutable; | 1 | package com.markozajc.akiwrapper.core.entities.impl.immutable; |
2 | 2 | ||
3 | import java.util.List; | ||
4 | import java.util.stream.Collectors; | ||
5 | |||
3 | import javax.annotation.Nonnull; | 6 | import javax.annotation.Nonnull; |
4 | 7 | ||
8 | import com.jcabi.xml.XML; | ||
5 | import com.markozajc.akiwrapper.core.entities.Server; | 9 | import com.markozajc.akiwrapper.core.entities.Server; |
6 | 10 | ||
7 | /** | 11 | /** |
@@ -11,32 +15,73 @@ import com.markozajc.akiwrapper.core.entities.Server; | |||
11 | */ | 15 | */ |
12 | public class ServerImpl implements Server { | 16 | public class ServerImpl implements Server { |
13 | 17 | ||
18 | private static final String LANGUAGE_ID_XPATH = "LANGUAGE/LANG_ID/text()"; // NOSONAR not a URL | ||
19 | private static final String SUBJECT_ID_XPATH = "SUBJECT/SUBJ_ID/text()"; // NOSONAR not a URL | ||
20 | private static final String CANDIDATE_URLS_XPATH = "CANDIDATS/*/text()"; // sic | ||
14 | @Nonnull | 21 | @Nonnull |
15 | private final String host; | 22 | private final String url; |
16 | @Nonnull | 23 | @Nonnull |
17 | private final Language localization; | 24 | private final Language localization; |
25 | @Nonnull | ||
26 | private final GuessType guessType; | ||
18 | 27 | ||
19 | /** | 28 | /** |
20 | * Creates a new instance of {@link ServerImpl}. | 29 | * Constructs a new instance of {@link ServerImpl}. |
21 | * | 30 | * |
22 | * @param host | 31 | * @param url |
23 | * server's host (for example {@code srv1.akinator.com.9100}}. | 32 | * server's URL (for example {@code https://srv3.akinator.com:9331/ws}). |
24 | * @param localization | 33 | * @param localization |
25 | * the localization language of this server | 34 | * server's language. |
35 | * @param guessType | ||
36 | * server's guess type. | ||
26 | */ | 37 | */ |
27 | public ServerImpl(@Nonnull String host, @Nonnull Language localization) { | 38 | public ServerImpl(@Nonnull String url, @Nonnull Language localization, @Nonnull GuessType guessType) { |
28 | this.host = host; | 39 | this.url = url; |
29 | this.localization = localization; | 40 | this.localization = localization; |
41 | this.guessType = guessType; | ||
42 | } | ||
43 | |||
44 | /** | ||
45 | * Constructs a {@link ServerImpl} from an {@code <INSTANCE>} XML node provided by | ||
46 | * the server-listing API endpoint. | ||
47 | * | ||
48 | * @param instance | ||
49 | * XML node. | ||
50 | * | ||
51 | * @return a {@link ServerImpl}. | ||
52 | */ | ||
53 | @SuppressWarnings("null") | ||
54 | @Nonnull | ||
55 | public static List<ServerImpl> fromXml(@Nonnull XML instance) { | ||
56 | String languageId = instance.xpath(LANGUAGE_ID_XPATH).get(0); | ||
57 | Language language = Language.getById(languageId); | ||
58 | if (language == null) | ||
59 | throw new IllegalStateException("'" + languageId + "' is not a recognized language."); | ||
60 | |||
61 | int guessTypeId = Integer.parseInt(instance.xpath(SUBJECT_ID_XPATH).get(0)); | ||
62 | GuessType guessType = GuessType.getById(guessTypeId); | ||
63 | if (guessType == null) | ||
64 | throw new IllegalStateException("'" + guessTypeId + "' is not a recognized guess type ID."); | ||
65 | |||
66 | return instance.xpath(CANDIDATE_URLS_XPATH) | ||
67 | .stream() | ||
68 | .map(host -> new ServerImpl(host, language, guessType)) | ||
69 | .collect(Collectors.toList()); | ||
30 | } | 70 | } |
31 | 71 | ||
32 | @Override | 72 | @Override |
33 | public Language getLocalization() { | 73 | public Language getLanguage() { |
34 | return this.localization; | 74 | return this.localization; |
35 | } | 75 | } |
36 | 76 | ||
37 | @Override | 77 | @Override |
38 | public String getHost() { | 78 | public GuessType getGuessType() { |
39 | return this.host; | 79 | return this.guessType; |
80 | } | ||
81 | |||
82 | @Override | ||
83 | public String getUrl() { | ||
84 | return this.url; | ||
40 | } | 85 | } |
41 | 86 | ||
42 | } | 87 | } |
diff --git a/src/main/java/com/markozajc/akiwrapper/core/entities/impl/immutable/ServerListImpl.java b/src/main/java/com/markozajc/akiwrapper/core/entities/impl/immutable/ServerListImpl.java new file mode 100644 index 0000000..c70fe8e --- /dev/null +++ b/src/main/java/com/markozajc/akiwrapper/core/entities/impl/immutable/ServerListImpl.java | |||
@@ -0,0 +1,93 @@ | |||
1 | package com.markozajc.akiwrapper.core.entities.impl.immutable; | ||
2 | |||
3 | import java.util.ArrayList; | ||
4 | import java.util.Arrays; | ||
5 | import java.util.Collection; | ||
6 | import java.util.List; | ||
7 | import java.util.Queue; | ||
8 | import java.util.concurrent.ConcurrentLinkedQueue; | ||
9 | import java.util.stream.Collectors; | ||
10 | import java.util.stream.Stream; | ||
11 | |||
12 | import javax.annotation.Nonnull; | ||
13 | |||
14 | import com.markozajc.akiwrapper.core.entities.Server; | ||
15 | import com.markozajc.akiwrapper.core.entities.ServerList; | ||
16 | |||
17 | public class ServerListImpl implements ServerList { | ||
18 | |||
19 | @Nonnull | ||
20 | private Server currentServer; | ||
21 | @Nonnull | ||
22 | private final Queue<Server> candidateServers; | ||
23 | |||
24 | @SuppressWarnings("null") | ||
25 | public ServerListImpl(@Nonnull Server first, @Nonnull Server... candidates) { | ||
26 | this(first, Arrays.asList(candidates)); | ||
27 | } | ||
28 | |||
29 | public ServerListImpl(@Nonnull Server first, @Nonnull Collection<Server> candidates) { | ||
30 | this.candidateServers = unwrapServersIntoQueue(candidates); | ||
31 | this.currentServer = first; | ||
32 | } | ||
33 | |||
34 | @SuppressWarnings("null") | ||
35 | public ServerListImpl(@Nonnull Collection<Server> servers) { | ||
36 | if (servers.isEmpty()) | ||
37 | throw new IllegalArgumentException("The collection of servers may not be empty"); | ||
38 | |||
39 | ConcurrentLinkedQueue<Server> queue = unwrapServersIntoQueue(servers); | ||
40 | this.candidateServers = queue; | ||
41 | this.currentServer = this.candidateServers.remove(); | ||
42 | } | ||
43 | |||
44 | @SuppressWarnings("null") | ||
45 | @Nonnull | ||
46 | private static ConcurrentLinkedQueue<Server> unwrapServersIntoQueue(@Nonnull Collection<Server> servers) { | ||
47 | return servers.stream().flatMap(s -> { | ||
48 | if (s instanceof ServerList) | ||
49 | return ((ServerList) s).getServers().stream(); | ||
50 | else | ||
51 | return Stream.of(s); | ||
52 | }).collect(Collectors.toCollection(ConcurrentLinkedQueue<Server>::new)); | ||
53 | } | ||
54 | |||
55 | @Override | ||
56 | public String getUrl() { | ||
57 | return this.currentServer.getUrl(); | ||
58 | } | ||
59 | |||
60 | @Override | ||
61 | public Language getLanguage() { | ||
62 | return this.currentServer.getLanguage(); | ||
63 | } | ||
64 | |||
65 | @Override | ||
66 | public GuessType getGuessType() { | ||
67 | return this.currentServer.getGuessType(); | ||
68 | } | ||
69 | |||
70 | @SuppressWarnings("null") | ||
71 | @Override | ||
72 | public boolean next() { | ||
73 | if (!hasNext()) | ||
74 | return false; | ||
75 | |||
76 | this.currentServer = this.candidateServers.remove(); | ||
77 | return true; | ||
78 | } | ||
79 | |||
80 | @Override | ||
81 | public List<Server> getServers() { | ||
82 | List<Server> result = new ArrayList<>(getRemainingSize() + 1); | ||
83 | result.add(this.currentServer); | ||
84 | result.addAll(this.candidateServers); | ||
85 | return result; | ||
86 | } | ||
87 | |||
88 | @Override | ||
89 | public int getRemainingSize() { | ||
90 | return this.candidateServers.size(); | ||
91 | } | ||
92 | |||
93 | } | ||
diff --git a/src/main/java/com/markozajc/akiwrapper/core/entities/impl/immutable/StatusImpl.java b/src/main/java/com/markozajc/akiwrapper/core/entities/impl/immutable/StatusImpl.java index 191bab1..390878c 100644 --- a/src/main/java/com/markozajc/akiwrapper/core/entities/impl/immutable/StatusImpl.java +++ b/src/main/java/com/markozajc/akiwrapper/core/entities/impl/immutable/StatusImpl.java | |||
@@ -1,8 +1,11 @@ | |||
1 | package com.markozajc.akiwrapper.core.entities.impl.immutable; | 1 | package com.markozajc.akiwrapper.core.entities.impl.immutable; |
2 | 2 | ||
3 | import javax.annotation.Nonnull; | ||
4 | import javax.annotation.Nullable; | ||
5 | |||
3 | import org.json.JSONObject; | 6 | import org.json.JSONObject; |
4 | 7 | ||
5 | import com.markozajc.akiwrapper.core.Route; | 8 | import com.google.gson.JsonObject; |
6 | import com.markozajc.akiwrapper.core.entities.Status; | 9 | import com.markozajc.akiwrapper.core.entities.Status; |
7 | 10 | ||
8 | /** | 11 | /** |
@@ -12,47 +15,52 @@ import com.markozajc.akiwrapper.core.entities.Status; | |||
12 | */ | 15 | */ |
13 | public class StatusImpl implements Status { | 16 | public class StatusImpl implements Status { |
14 | 17 | ||
15 | private static final String STATUS_FORMAT = "%s - %s"; | 18 | private static final long serialVersionUID = 1; |
19 | |||
20 | private static final String DIVIDER = " - "; | ||
21 | private static final String STATUS_FORMAT = "%s" + DIVIDER + "%s"; | ||
16 | 22 | ||
23 | @Nullable | ||
17 | private final String reason; | 24 | private final String reason; |
25 | @Nonnull | ||
18 | private final Level level; | 26 | private final Level level; |
19 | 27 | ||
20 | /** | 28 | /** |
21 | * Creates a new {@link StatusImpl} instance from raw parameters. | 29 | * Constructs a new {@link StatusImpl} instance from raw parameters. |
22 | * | 30 | * |
23 | * @param completion | 31 | * @param completion |
24 | */ | 32 | */ |
25 | public StatusImpl(String completion) { | 33 | public StatusImpl(@Nonnull String completion) { |
26 | if (completion.toLowerCase().startsWith("ok")) { | 34 | this.level = determineLevel(completion); |
27 | this.level = Level.OK; | 35 | this.reason = determineReason(completion); |
28 | this.reason = null; | ||
29 | |||
30 | } else { | ||
31 | this.reason = completion.split(" - ", 2)[1]; | ||
32 | |||
33 | if (completion.toLowerCase().startsWith("warn")) { | ||
34 | this.level = Level.WARNING; | ||
35 | |||
36 | } else if (completion.toLowerCase().startsWith("ko")) { | ||
37 | this.level = Level.ERROR; | ||
38 | |||
39 | } else { | ||
40 | this.level = Level.UNKNOWN; | ||
41 | } | ||
42 | } | ||
43 | } | 36 | } |
44 | 37 | ||
45 | /** | 38 | /** |
46 | * Creates a new {@link StatusImpl} instance. | 39 | * Constructs a new {@link StatusImpl} instance from a {@link JsonObject}. |
47 | * | 40 | * |
48 | * @param json | 41 | * @param json |
49 | * completion level (acquired with (Any {@link Route}) > | ||
50 | * {@link JSONObject} completion) | ||
51 | */ | 42 | */ |
52 | public StatusImpl(JSONObject json) { | 43 | @SuppressWarnings("null") |
44 | public StatusImpl(@Nonnull JSONObject json) { | ||
53 | this(json.getString("completion")); | 45 | this(json.getString("completion")); |
54 | } | 46 | } |
55 | 47 | ||
48 | @Nullable | ||
49 | private static String determineReason(@Nonnull String completion) { | ||
50 | int reasonSplitIndex = completion.indexOf(DIVIDER); | ||
51 | if (reasonSplitIndex != -1) | ||
52 | return completion.substring(reasonSplitIndex + DIVIDER.length()); | ||
53 | return null; | ||
54 | } | ||
55 | |||
56 | @Nonnull | ||
57 | private static Level determineLevel(@Nonnull String completion) { | ||
58 | for (Level iteratedLevel : Level.values()) | ||
59 | if (completion.toLowerCase().startsWith(iteratedLevel.toString().toLowerCase())) | ||
60 | return iteratedLevel; | ||
61 | return Level.UNKNOWN; | ||
62 | } | ||
63 | |||
56 | @Override | 64 | @Override |
57 | public String getReason() { | 65 | public String getReason() { |
58 | return this.reason; | 66 | return this.reason; |
@@ -63,10 +71,12 @@ public class StatusImpl implements Status { | |||
63 | return this.level; | 71 | return this.level; |
64 | } | 72 | } |
65 | 73 | ||
66 | |||
67 | @Override | 74 | @Override |
68 | public String toString() { | 75 | public String toString() { |
69 | return String.format(STATUS_FORMAT, this.level.toString(), this.reason); | 76 | if (getReason() == null) |
77 | return getLevel().toString(); | ||
78 | else | ||
79 | return String.format(STATUS_FORMAT, getLevel().toString(), getReason()); | ||
70 | } | 80 | } |
71 | 81 | ||
72 | } | 82 | } |
diff --git a/src/main/java/com/markozajc/akiwrapper/core/entities/impl/mutable/MutableAkiwrapperMetadata.java b/src/main/java/com/markozajc/akiwrapper/core/entities/impl/mutable/MutableAkiwrapperMetadata.java index de04137..8476ff5 100644 --- a/src/main/java/com/markozajc/akiwrapper/core/entities/impl/mutable/MutableAkiwrapperMetadata.java +++ b/src/main/java/com/markozajc/akiwrapper/core/entities/impl/mutable/MutableAkiwrapperMetadata.java | |||
@@ -4,10 +4,13 @@ import javax.annotation.Nonnull; | |||
4 | import javax.annotation.Nullable; | 4 | import javax.annotation.Nullable; |
5 | 5 | ||
6 | import com.markozajc.akiwrapper.core.entities.AkiwrapperMetadata; | 6 | import com.markozajc.akiwrapper.core.entities.AkiwrapperMetadata; |
7 | import com.markozajc.akiwrapper.core.entities.Guess; | ||
7 | import com.markozajc.akiwrapper.core.entities.Question; | 8 | import com.markozajc.akiwrapper.core.entities.Question; |
8 | import com.markozajc.akiwrapper.core.entities.Server; | 9 | import com.markozajc.akiwrapper.core.entities.Server; |
10 | import com.markozajc.akiwrapper.core.entities.Server.GuessType; | ||
9 | import com.markozajc.akiwrapper.core.entities.Server.Language; | 11 | import com.markozajc.akiwrapper.core.entities.Server.Language; |
10 | import com.markozajc.akiwrapper.core.entities.ServerGroup; | 12 | import com.markozajc.akiwrapper.core.entities.ServerList; |
13 | import com.markozajc.akiwrapper.core.entities.impl.immutable.ImmutableAkiwrapperMetadata; | ||
11 | import com.markozajc.akiwrapper.core.utils.Servers; | 14 | import com.markozajc.akiwrapper.core.utils.Servers; |
12 | 15 | ||
13 | /** | 16 | /** |
@@ -17,74 +20,32 @@ import com.markozajc.akiwrapper.core.utils.Servers; | |||
17 | */ | 20 | */ |
18 | public abstract class MutableAkiwrapperMetadata extends AkiwrapperMetadata { | 21 | public abstract class MutableAkiwrapperMetadata extends AkiwrapperMetadata { |
19 | 22 | ||
20 | @Nonnull | 23 | @Nullable |
21 | protected String name; | ||
22 | @Nonnull | ||
23 | protected String userAgent; | ||
24 | protected Server server; | 24 | protected Server server; |
25 | protected boolean filterProfanity; | 25 | protected boolean filterProfanity; |
26 | @Nonnull | 26 | @Nonnull |
27 | protected Language localization; | 27 | protected Language language; |
28 | @Nonnull | ||
29 | protected GuessType guessType; | ||
28 | 30 | ||
29 | /** | 31 | /** |
30 | * Creates a new {@link MutableAkiwrapperMetadata} instance. | 32 | * Creates a new {@link ImmutableAkiwrapperMetadata} instance. |
31 | * | 33 | * |
32 | * @param server | 34 | * @param server |
33 | * the API server to use | 35 | * API server that the requests will be sent to. |
34 | * @param name | ||
35 | * player's name (won't have any huge impact but is still passed to the | ||
36 | * Akinator API for convenience) | ||
37 | * @param userAgent | ||
38 | * the user-agent to use | ||
39 | * @param filterProfanity | 36 | * @param filterProfanity |
40 | * whether to filter out all profanity elements | 37 | * whether to filter out NSFW {@link Question}s and {@link Guess}es. |
41 | * @param localization | 38 | * @param language |
42 | * the localization language that will be passed to the API server. This | 39 | * {@link Language} of {@link Question}s. |
43 | * affects textual elements such as {@link Question}-s | 40 | * @param guessType |
41 | * {@link GuessType} of {@link Guess}es. | ||
44 | */ | 42 | */ |
45 | public MutableAkiwrapperMetadata(@Nonnull String name, @Nonnull String userAgent, @Nullable Server server, | 43 | public MutableAkiwrapperMetadata(@Nullable Server server, boolean filterProfanity, @Nonnull Language language, |
46 | boolean filterProfanity, @Nonnull Language localization) { | 44 | @Nonnull GuessType guessType) { |
47 | this.name = name; | ||
48 | this.userAgent = userAgent; | ||
49 | this.server = server; | 45 | this.server = server; |
50 | this.filterProfanity = filterProfanity; | 46 | this.filterProfanity = filterProfanity; |
51 | this.localization = localization; | 47 | this.language = language; |
52 | } | 48 | this.guessType = guessType; |
53 | |||
54 | @Override | ||
55 | public String getName() { | ||
56 | return this.name; | ||
57 | } | ||
58 | |||
59 | /** | ||
60 | * Sets user's name. | ||
61 | * | ||
62 | * @param name | ||
63 | * @return current instance, used for chaining | ||
64 | * @see #getName() | ||
65 | */ | ||
66 | public MutableAkiwrapperMetadata setName(@Nonnull String name) { | ||
67 | this.name = name; | ||
68 | |||
69 | return this; | ||
70 | } | ||
71 | |||
72 | @Override | ||
73 | public String getUserAgent() { | ||
74 | return this.userAgent; | ||
75 | } | ||
76 | |||
77 | /** | ||
78 | * Sets the user-agent. | ||
79 | * | ||
80 | * @param userAgent | ||
81 | * @return current instance, used for chaining | ||
82 | * @see #getUserAgent() | ||
83 | */ | ||
84 | public MutableAkiwrapperMetadata setUserAgent(@Nonnull String userAgent) { | ||
85 | this.userAgent = userAgent; | ||
86 | |||
87 | return this; | ||
88 | } | 49 | } |
89 | 50 | ||
90 | @Override | 51 | @Override |
@@ -93,17 +54,27 @@ public abstract class MutableAkiwrapperMetadata extends AkiwrapperMetadata { | |||
93 | } | 54 | } |
94 | 55 | ||
95 | /** | 56 | /** |
96 | * Sets the API server. | 57 | * Sets the {@link Server} or (recommended) a {@link ServerList}. It is not |
58 | * recommended to set the {@link Server} manually (unless for debugging purposes or | ||
59 | * as some kind of workaround where Akiwrapper's server finder fails) as Akiwrapper | ||
60 | * already does its best to find the most suitable one. <br> | ||
61 | * <b>Caution!</b> Setting the server to a non-null value overwrites the | ||
62 | * {@link Language} and the {@link GuessType} with the given {@link Server}'s values. | ||
97 | * | 63 | * |
98 | * @param server | 64 | * @param server |
65 | * | ||
99 | * @return current instance, used for chaining | 66 | * @return current instance, used for chaining |
67 | * | ||
100 | * @see #getServer() | 68 | * @see #getServer() |
101 | * @see Servers#SERVER_GROUPS | 69 | * @see Servers#findServers(Language, GuessType) |
102 | * @see ServerGroup#getFirstAvailableServer() | ||
103 | */ | 70 | */ |
104 | @Nonnull | 71 | @Nonnull |
105 | public MutableAkiwrapperMetadata setServer(@Nullable Server server) { | 72 | public MutableAkiwrapperMetadata setServer(@Nullable Server server) { |
106 | this.server = server; | 73 | this.server = server; |
74 | if (server != null) { | ||
75 | this.language = server.getLanguage(); | ||
76 | this.guessType = server.getGuessType(); | ||
77 | } | ||
107 | 78 | ||
108 | return this; | 79 | return this; |
109 | } | 80 | } |
@@ -117,7 +88,9 @@ public abstract class MutableAkiwrapperMetadata extends AkiwrapperMetadata { | |||
117 | * Sets the "filter profanity" mode. | 88 | * Sets the "filter profanity" mode. |
118 | * | 89 | * |
119 | * @param filterProfanity | 90 | * @param filterProfanity |
91 | * | ||
120 | * @return current instance, used for chaining | 92 | * @return current instance, used for chaining |
93 | * | ||
121 | * @see #doesFilterProfanity() | 94 | * @see #doesFilterProfanity() |
122 | */ | 95 | */ |
123 | @Nonnull | 96 | @Nonnull |
@@ -128,20 +101,49 @@ public abstract class MutableAkiwrapperMetadata extends AkiwrapperMetadata { | |||
128 | } | 101 | } |
129 | 102 | ||
130 | @Override | 103 | @Override |
131 | public Language getLocalization() { | 104 | public Language getLanguage() { |
132 | return this.localization; | 105 | return this.language; |
106 | } | ||
107 | |||
108 | /** | ||
109 | * Sets the {@link Language}.<br> | ||
110 | * <b>Caution!</b> Setting the {@link Language} will set the {@link Server} to | ||
111 | * {@code null} (meaning it will be automatically selected). | ||
112 | * | ||
113 | * @param language | ||
114 | * | ||
115 | * @return current instance, used for chaining | ||
116 | * | ||
117 | * @see #getLanguage() | ||
118 | */ | ||
119 | @Nonnull | ||
120 | public MutableAkiwrapperMetadata setLanguage(@Nonnull Language language) { | ||
121 | this.language = language; | ||
122 | this.server = null; | ||
123 | |||
124 | return this; | ||
125 | } | ||
126 | |||
127 | @Override | ||
128 | public GuessType getGuessType() { | ||
129 | return this.guessType; | ||
133 | } | 130 | } |
134 | 131 | ||
135 | /** | 132 | /** |
136 | * Sets the localization language. | 133 | * Sets the {@link GuessType}.<br> |
134 | * <b>Caution!</b> Setting the {@link Language} will set the {@link Server} to | ||
135 | * {@code null} (meaning it will be automatically selected). | ||
136 | * | ||
137 | * @param guessType | ||
137 | * | 138 | * |
138 | * @param localization | ||
139 | * @return current instance, used for chaining | 139 | * @return current instance, used for chaining |
140 | * @see #getLocalization() | 140 | * |
141 | * @see #getLanguage() | ||
141 | */ | 142 | */ |
142 | @Nonnull | 143 | @Nonnull |
143 | public MutableAkiwrapperMetadata setLocalization(@Nonnull Language localization) { | 144 | public MutableAkiwrapperMetadata setGuessType(@Nonnull GuessType guessType) { |
144 | this.localization = localization; | 145 | this.guessType = guessType; |
146 | this.server = null; | ||
145 | 147 | ||
146 | return this; | 148 | return this; |
147 | } | 149 | } |
diff --git a/src/main/java/com/markozajc/akiwrapper/core/exceptions/MissingQuestionException.java b/src/main/java/com/markozajc/akiwrapper/core/exceptions/MissingQuestionException.java index 3700026..09cb5b5 100644 --- a/src/main/java/com/markozajc/akiwrapper/core/exceptions/MissingQuestionException.java +++ b/src/main/java/com/markozajc/akiwrapper/core/exceptions/MissingQuestionException.java | |||
@@ -1,14 +1,14 @@ | |||
1 | package com.markozajc.akiwrapper.core.exceptions; | 1 | package com.markozajc.akiwrapper.core.exceptions; |
2 | 2 | ||
3 | /** | 3 | /** |
4 | * An exception that signals there is no question left to answer or fetch. | 4 | * An exception indicating that there is no question left to answer or fetch. |
5 | * | 5 | * |
6 | * @author Marko Zajc | 6 | * @author Marko Zajc |
7 | */ | 7 | */ |
8 | public class MissingQuestionException extends RuntimeException { | 8 | public class MissingQuestionException extends RuntimeException { |
9 | 9 | ||
10 | /** | 10 | /** |
11 | * Creates a new {@link MissingQuestionException} instance. | 11 | * Constructs a new {@link MissingQuestionException} instance. |
12 | */ | 12 | */ |
13 | public MissingQuestionException() { | 13 | public MissingQuestionException() { |
14 | super(); | 14 | super(); |
diff --git a/src/main/java/com/markozajc/akiwrapper/core/exceptions/ServerGroupUnavailableException.java b/src/main/java/com/markozajc/akiwrapper/core/exceptions/ServerGroupUnavailableException.java deleted file mode 100644 index b468b33..0000000 --- a/src/main/java/com/markozajc/akiwrapper/core/exceptions/ServerGroupUnavailableException.java +++ /dev/null | |||
@@ -1,21 +0,0 @@ | |||
1 | package com.markozajc.akiwrapper.core.exceptions; | ||
2 | |||
3 | import com.markozajc.akiwrapper.core.entities.ServerGroup; | ||
4 | |||
5 | /** | ||
6 | * An exception that signals that all servers from a {@link ServerGroup} are | ||
7 | * unavailable. | ||
8 | */ | ||
9 | public class ServerGroupUnavailableException extends ServerUnavailableException { | ||
10 | |||
11 | /** | ||
12 | * Creates a new {@link ServerGroupUnavailableException}. | ||
13 | * | ||
14 | * @param sg | ||
15 | * the unavailable {@link ServerGroup}. | ||
16 | */ | ||
17 | public ServerGroupUnavailableException(ServerGroup sg) { | ||
18 | super(sg.getServers()); | ||
19 | } | ||
20 | |||
21 | } | ||
diff --git a/src/main/java/com/markozajc/akiwrapper/core/exceptions/ServerNotFoundException.java b/src/main/java/com/markozajc/akiwrapper/core/exceptions/ServerNotFoundException.java new file mode 100644 index 0000000..8f339a4 --- /dev/null +++ b/src/main/java/com/markozajc/akiwrapper/core/exceptions/ServerNotFoundException.java | |||
@@ -0,0 +1,22 @@ | |||
1 | package com.markozajc.akiwrapper.core.exceptions; | ||
2 | |||
3 | import com.markozajc.akiwrapper.core.entities.Server; | ||
4 | import com.markozajc.akiwrapper.core.entities.Server.GuessType; | ||
5 | import com.markozajc.akiwrapper.core.entities.Server.Language; | ||
6 | |||
7 | /** | ||
8 | * An exception indicating that no {@link Server} could be found for the given | ||
9 | * combination of {@link Language} and {@link GuessType}. | ||
10 | * | ||
11 | * @author Marko Zajc | ||
12 | */ | ||
13 | public class ServerNotFoundException extends Exception { | ||
14 | |||
15 | /** | ||
16 | * Constructs a new {@link ServerNotFoundException}. | ||
17 | */ | ||
18 | public ServerNotFoundException() { | ||
19 | super(); | ||
20 | } | ||
21 | |||
22 | } | ||
diff --git a/src/main/java/com/markozajc/akiwrapper/core/exceptions/ServerUnavailableException.java b/src/main/java/com/markozajc/akiwrapper/core/exceptions/ServerUnavailableException.java index 7353e3a..2eadabe 100644 --- a/src/main/java/com/markozajc/akiwrapper/core/exceptions/ServerUnavailableException.java +++ b/src/main/java/com/markozajc/akiwrapper/core/exceptions/ServerUnavailableException.java | |||
@@ -1,45 +1,36 @@ | |||
1 | package com.markozajc.akiwrapper.core.exceptions; | 1 | package com.markozajc.akiwrapper.core.exceptions; |
2 | 2 | ||
3 | import java.util.Collection; | 3 | import javax.annotation.Nonnull; |
4 | import java.util.stream.Collectors; | ||
5 | 4 | ||
6 | import com.markozajc.akiwrapper.core.entities.Server; | 5 | import com.markozajc.akiwrapper.core.entities.Server; |
6 | import com.markozajc.akiwrapper.core.entities.Status; | ||
7 | import com.markozajc.akiwrapper.core.entities.impl.immutable.StatusImpl; | 7 | import com.markozajc.akiwrapper.core.entities.impl.immutable.StatusImpl; |
8 | 8 | ||
9 | /** | 9 | /** |
10 | * An exception representing that the currently used {@link Server} has gone offline. | 10 | * An exception indicating that the currently used {@link Server} has gone offline. |
11 | * | ||
12 | * @author Marko Zajc | ||
11 | */ | 13 | */ |
12 | public class ServerUnavailableException extends StatusException { | 14 | public class ServerUnavailableException extends StatusException { |
13 | 15 | ||
14 | private final String serverUrl; | ||
15 | |||
16 | /** | ||
17 | * Creates a new {@link ServerUnavailableException} instance for a single server. | ||
18 | * | ||
19 | * @param server | ||
20 | */ | ||
21 | public ServerUnavailableException(Server server) { | ||
22 | super(new StatusImpl("KO - SERVER DOWN")); | ||
23 | this.serverUrl = server.getApiUrl(); | ||
24 | } | ||
25 | |||
26 | /** | 16 | /** |
27 | * Creates a new {@link ServerUnavailableException} instance for multiple servers. | 17 | * Constructs a new {@link ServerUnavailableException} from a {@link Status}. |
28 | * | 18 | * |
29 | * @param servers | 19 | * @param status |
20 | * erroneous status. | ||
30 | */ | 21 | */ |
31 | public ServerUnavailableException(Collection<Server> servers) { | 22 | public ServerUnavailableException(@Nonnull Status status) { |
32 | super(new StatusImpl("KO - SERVER DOWN")); | 23 | super(status); |
33 | this.serverUrl = servers.stream().map(Server::getApiUrl).collect(Collectors.joining(", ")); | ||
34 | } | 24 | } |
35 | 25 | ||
36 | /** | 26 | /** |
37 | * Returns the URL of the API server that went down | 27 | * Constructs a new {@link ServerUnavailableException} from a {@link Status} string. |
38 | * | 28 | * |
39 | * @return API server's URL | 29 | * @param status |
30 | * erroneous status string. | ||
40 | */ | 31 | */ |
41 | public String getServerUrl() { | 32 | public ServerUnavailableException(@Nonnull String status) { |
42 | return this.serverUrl; | 33 | super(new StatusImpl(status)); |
43 | } | 34 | } |
44 | 35 | ||
45 | } | 36 | } |
diff --git a/src/main/java/com/markozajc/akiwrapper/core/exceptions/StatusException.java b/src/main/java/com/markozajc/akiwrapper/core/exceptions/StatusException.java index 03cd31e..9e3e02a 100644 --- a/src/main/java/com/markozajc/akiwrapper/core/exceptions/StatusException.java +++ b/src/main/java/com/markozajc/akiwrapper/core/exceptions/StatusException.java | |||
@@ -3,7 +3,7 @@ package com.markozajc.akiwrapper.core.exceptions; | |||
3 | import com.markozajc.akiwrapper.core.entities.Status; | 3 | import com.markozajc.akiwrapper.core.entities.Status; |
4 | 4 | ||
5 | /** | 5 | /** |
6 | * An exception signaling that the server returned an error code ("KO"). | 6 | * An exception indicating that the server returned an error code ("KO"). |
7 | * | 7 | * |
8 | * @author Marko Zajc | 8 | * @author Marko Zajc |
9 | */ | 9 | */ |
@@ -12,7 +12,7 @@ public class StatusException extends RuntimeException { | |||
12 | private final Status status; | 12 | private final Status status; |
13 | 13 | ||
14 | /** | 14 | /** |
15 | * Creates a new {@link StatusException}. | 15 | * Constructs a new {@link StatusException}. |
16 | * | 16 | * |
17 | * @param status | 17 | * @param status |
18 | * status to append | 18 | * status to append |
diff --git a/src/main/java/com/markozajc/akiwrapper/core/impl/AkiwrapperImpl.java b/src/main/java/com/markozajc/akiwrapper/core/impl/AkiwrapperImpl.java index 4131ba5..f8d90de 100644 --- a/src/main/java/com/markozajc/akiwrapper/core/impl/AkiwrapperImpl.java +++ b/src/main/java/com/markozajc/akiwrapper/core/impl/AkiwrapperImpl.java | |||
@@ -1,6 +1,5 @@ | |||
1 | package com.markozajc.akiwrapper.core.impl; | 1 | package com.markozajc.akiwrapper.core.impl; |
2 | 2 | ||
3 | import java.io.IOException; | ||
4 | import java.util.ArrayList; | 3 | import java.util.ArrayList; |
5 | import java.util.Collections; | 4 | import java.util.Collections; |
6 | import java.util.List; | 5 | import java.util.List; |
@@ -15,18 +14,16 @@ import org.json.JSONObject; | |||
15 | import com.markozajc.akiwrapper.Akiwrapper; | 14 | import com.markozajc.akiwrapper.Akiwrapper; |
16 | import com.markozajc.akiwrapper.AkiwrapperBuilder; | 15 | import com.markozajc.akiwrapper.AkiwrapperBuilder; |
17 | import com.markozajc.akiwrapper.core.Route; | 16 | import com.markozajc.akiwrapper.core.Route; |
18 | import com.markozajc.akiwrapper.core.entities.AkiwrapperMetadata; | ||
19 | import com.markozajc.akiwrapper.core.entities.Guess; | 17 | import com.markozajc.akiwrapper.core.entities.Guess; |
20 | import com.markozajc.akiwrapper.core.entities.Question; | 18 | import com.markozajc.akiwrapper.core.entities.Question; |
21 | import com.markozajc.akiwrapper.core.entities.Server; | 19 | import com.markozajc.akiwrapper.core.entities.Server; |
20 | import com.markozajc.akiwrapper.core.entities.ServerList; | ||
22 | import com.markozajc.akiwrapper.core.entities.Status.Level; | 21 | import com.markozajc.akiwrapper.core.entities.Status.Level; |
23 | import com.markozajc.akiwrapper.core.entities.impl.immutable.GuessImpl; | 22 | import com.markozajc.akiwrapper.core.entities.impl.immutable.GuessImpl; |
24 | import com.markozajc.akiwrapper.core.entities.impl.immutable.QuestionImpl; | 23 | import com.markozajc.akiwrapper.core.entities.impl.immutable.QuestionImpl; |
25 | import com.markozajc.akiwrapper.core.entities.impl.immutable.StatusImpl; | 24 | import com.markozajc.akiwrapper.core.entities.impl.immutable.StatusImpl; |
26 | import com.markozajc.akiwrapper.core.exceptions.MissingQuestionException; | 25 | import com.markozajc.akiwrapper.core.exceptions.MissingQuestionException; |
27 | import com.markozajc.akiwrapper.core.exceptions.ServerGroupUnavailableException; | ||
28 | import com.markozajc.akiwrapper.core.exceptions.StatusException; | 26 | import com.markozajc.akiwrapper.core.exceptions.StatusException; |
29 | import com.markozajc.akiwrapper.core.utils.Servers; | ||
30 | 27 | ||
31 | /** | 28 | /** |
32 | * An implementation of {@link Akiwrapper}. | 29 | * An implementation of {@link Akiwrapper}. |
@@ -35,10 +32,11 @@ import com.markozajc.akiwrapper.core.utils.Servers; | |||
35 | */ | 32 | */ |
36 | public class AkiwrapperImpl implements Akiwrapper { | 33 | public class AkiwrapperImpl implements Akiwrapper { |
37 | 34 | ||
35 | private static final String NO_MORE_QUESTIONS_STATUS = "elem list is empty"; | ||
38 | private static final String PARAMETERS_KEY = "parameters"; | 36 | private static final String PARAMETERS_KEY = "parameters"; |
39 | 37 | ||
40 | /** | 38 | /** |
41 | * A class used to define the temporary API token. | 39 | * A class used to define the session token. |
42 | * | 40 | * |
43 | * @author Marko Zajc | 41 | * @author Marko Zajc |
44 | */ | 42 | */ |
@@ -78,8 +76,6 @@ public class AkiwrapperImpl implements Akiwrapper { | |||
78 | } | 76 | } |
79 | 77 | ||
80 | @Nonnull | 78 | @Nonnull |
81 | private final String userAgent; | ||
82 | @Nonnull | ||
83 | private final Server server; | 79 | private final Server server; |
84 | private final boolean filterProfanity; | 80 | private final boolean filterProfanity; |
85 | @Nonnull | 81 | @Nonnull |
@@ -90,74 +86,49 @@ public class AkiwrapperImpl implements Akiwrapper { | |||
90 | private Question currentQuestion; | 86 | private Question currentQuestion; |
91 | 87 | ||
92 | /** | 88 | /** |
93 | * Creates a new Akiwrapper and registers a new API session. The first question can | 89 | * Constructs a new {@link Akiwrapper} instance and creates a new API session. The |
94 | * be retrieved with {@link #getCurrentQuestion()}. | 90 | * first question can be retrieved with {@link #getCurrentQuestion()}. |
95 | * | ||
96 | * @param metadata | ||
97 | * metadata to use. All {@code null} values will be replaced with the | ||
98 | * default values (you can see defaults at {@link AkiwrapperBuilder}'s | ||
99 | * getters) | ||
100 | * | 91 | * |
101 | * @throws ServerGroupUnavailableException | 92 | * @param server |
102 | * if no API server is available | 93 | * {@link Server} to use. Does not work with a {@link ServerList}, |
103 | * @throws IllegalArgumentException | 94 | * {@link AkiwrapperBuilder} implements that functionality. |
104 | * is {@code metadata} is null | 95 | * @param filterProfanity |
96 | * whether to tell API to filter profanity. | ||
105 | */ | 97 | */ |
106 | @SuppressWarnings("null") | 98 | @SuppressWarnings("null") |
107 | public AkiwrapperImpl(@Nonnull AkiwrapperMetadata metadata) { | 99 | public AkiwrapperImpl(@Nonnull Server server, boolean filterProfanity) { |
108 | Server serverCopy = metadata.getServer(); | 100 | JSONObject question = Route.NEW_SESSION |
109 | if (serverCopy == null) | 101 | .getRequest("", filterProfanity, Long.toString(System.currentTimeMillis()), server.getUrl()) |
110 | serverCopy = Servers.getFirstAvailableServer(metadata.getLocalization()); | 102 | .getJSON(); |
111 | 103 | JSONObject parameters = question.getJSONObject(PARAMETERS_KEY); | |
112 | this.server = serverCopy; | ||
113 | // Checks & sets the server | ||
114 | |||
115 | this.userAgent = metadata.getUserAgent(); | ||
116 | // Checks & sets the user-agent | ||
117 | |||
118 | this.filterProfanity = metadata.doesFilterProfanity(); | ||
119 | // Sets the profanity filter | ||
120 | |||
121 | JSONObject question; | ||
122 | String name = metadata.getName(); | ||
123 | |||
124 | try { | ||
125 | question = Route.NEW_SESSION.getRequest(this.server.getApiUrl(), this.filterProfanity, name).getJSON(); | ||
126 | } catch (IOException e) { | ||
127 | // Shouldn't happen, the server was requested before | ||
128 | throw new IllegalStateException(e); | ||
129 | } | ||
130 | // Checks & uses the name | ||
131 | |||
132 | JSONObject identification = question.getJSONObject(PARAMETERS_KEY).getJSONObject("identification"); | ||
133 | |||
134 | this.token = new Token(Long.parseLong(identification.getString("signature")), | ||
135 | Integer.parseInt(identification.getString("session"))); | ||
136 | |||
137 | this.currentQuestion = new QuestionImpl( | ||
138 | question.getJSONObject(PARAMETERS_KEY).getJSONObject("step_information"), new StatusImpl("OK") | ||
139 | /* | ||
140 | * We can assume that the completion is OK because if it wouldn't be, calling the | ||
141 | * Route.NEW_SESSION would have thrown ServerUnavailableException | ||
142 | */ | ||
143 | ); | ||
144 | 104 | ||
105 | this.token = getToken(parameters); | ||
106 | this.currentQuestion = new QuestionImpl(parameters.getJSONObject("step_information"), new StatusImpl("OK")); | ||
107 | this.filterProfanity = filterProfanity; | ||
108 | this.server = server; | ||
145 | this.currentStep = 0; | 109 | this.currentStep = 0; |
146 | } | 110 | } |
147 | 111 | ||
112 | @Nonnull | ||
113 | private static Token getToken(@Nonnull JSONObject parameters) { | ||
114 | JSONObject identification = parameters.getJSONObject("identification"); | ||
115 | return new Token(Long.parseLong(identification.getString("signature")), | ||
116 | Integer.parseInt(identification.getString("session"))); | ||
117 | } | ||
118 | |||
148 | @SuppressWarnings("null") | 119 | @SuppressWarnings("null") |
149 | @Override | 120 | @Override |
150 | public Question answerCurrentQuestion(Answer answer) throws IOException { | 121 | public Question answerCurrentQuestion(Answer answer) { |
151 | Question currentQuestion2 = this.currentQuestion; | 122 | Question currentQuestion2 = this.currentQuestion; |
152 | if (currentQuestion2 != null) { | 123 | if (currentQuestion2 != null) { |
153 | JSONObject question = Route.ANSWER | 124 | JSONObject question = Route.ANSWER |
154 | .getRequest(this.server.getApiUrl(), this.filterProfanity, this.token, "" + currentQuestion2.getStep(), | 125 | .getRequest(this.server.getUrl(), this.filterProfanity, this.token, "" + currentQuestion2.getStep(), |
155 | "" + answer.getId()) | 126 | "" + answer.getId()) |
156 | .getJSON(); | 127 | .getJSON(); |
157 | try { | 128 | try { |
158 | this.currentQuestion = new QuestionImpl(question.getJSONObject(PARAMETERS_KEY), | 129 | this.currentQuestion = |
159 | new StatusImpl(question)); | 130 | new QuestionImpl(question.getJSONObject(PARAMETERS_KEY), new StatusImpl(question)); |
160 | } catch (MissingQuestionException e) { | 131 | } catch (MissingQuestionException e) { // NOSONAR It does not need to be logged |
161 | this.currentQuestion = null; | 132 | this.currentQuestion = null; |
162 | return null; | 133 | return null; |
163 | } | 134 | } |
@@ -171,7 +142,7 @@ public class AkiwrapperImpl implements Akiwrapper { | |||
171 | 142 | ||
172 | @SuppressWarnings("null") | 143 | @SuppressWarnings("null") |
173 | @Override | 144 | @Override |
174 | public Question undoAnswer() throws IOException { | 145 | public Question undoAnswer() { |
175 | Question current = getCurrentQuestion(); | 146 | Question current = getCurrentQuestion(); |
176 | if (current == null) | 147 | if (current == null) |
177 | return null; | 148 | return null; |
@@ -180,7 +151,7 @@ public class AkiwrapperImpl implements Akiwrapper { | |||
180 | return null; | 151 | return null; |
181 | 152 | ||
182 | JSONObject question = Route.CANCEL_ANSWER | 153 | JSONObject question = Route.CANCEL_ANSWER |
183 | .getRequest(this.server.getApiUrl(), this.filterProfanity, this.token, Integer.toString(current.getStep())) | 154 | .getRequest(this.server.getUrl(), this.filterProfanity, this.token, Integer.toString(current.getStep())) |
184 | .getJSON(); | 155 | .getJSON(); |
185 | 156 | ||
186 | this.currentQuestion = new QuestionImpl(question.getJSONObject(PARAMETERS_KEY), new StatusImpl(question)); | 157 | this.currentQuestion = new QuestionImpl(question.getJSONObject(PARAMETERS_KEY), new StatusImpl(question)); |
@@ -196,15 +167,14 @@ public class AkiwrapperImpl implements Akiwrapper { | |||
196 | 167 | ||
197 | @SuppressWarnings("null") | 168 | @SuppressWarnings("null") |
198 | @Override | 169 | @Override |
199 | public List<Guess> getGuesses() throws IOException { | 170 | public List<Guess> getGuesses() { |
200 | JSONObject list = null; | 171 | JSONObject list = null; |
201 | try { | 172 | try { |
202 | list = Route.LIST.setUserAgent(this.userAgent) | 173 | list = Route.LIST.getRequest(this.server.getUrl(), this.filterProfanity, this.token, "" + this.currentStep) |
203 | .getRequest(this.server.getApiUrl(), this.filterProfanity, this.token, "" + this.currentStep) | ||
204 | .getJSON(); | 174 | .getJSON(); |
205 | } catch (StatusException e) { | 175 | } catch (StatusException e) { |
206 | if (e.getStatus().getLevel().equals(Level.ERROR) | 176 | if (e.getStatus().getLevel() == Level.ERROR |
207 | && e.getStatus().getReason().equalsIgnoreCase("elem list is empty")) { | 177 | && NO_MORE_QUESTIONS_STATUS.equalsIgnoreCase(e.getStatus().getReason())) { |
208 | return Collections.unmodifiableList(new ArrayList<>()); | 178 | return Collections.unmodifiableList(new ArrayList<>()); |
209 | } | 179 | } |
210 | 180 | ||
@@ -227,11 +197,4 @@ public class AkiwrapperImpl implements Akiwrapper { | |||
227 | return this.server; | 197 | return this.server; |
228 | } | 198 | } |
229 | 199 | ||
230 | /** | ||
231 | * @return the currently used user-agent | ||
232 | */ | ||
233 | public String getUserAgent() { | ||
234 | return this.userAgent; | ||
235 | } | ||
236 | |||
237 | } | 200 | } |
diff --git a/src/main/java/com/markozajc/akiwrapper/core/utils/HTTPUtils.java b/src/main/java/com/markozajc/akiwrapper/core/utils/HTTPUtils.java deleted file mode 100644 index 1cb7e94..0000000 --- a/src/main/java/com/markozajc/akiwrapper/core/utils/HTTPUtils.java +++ /dev/null | |||
@@ -1,41 +0,0 @@ | |||
1 | package com.markozajc.akiwrapper.core.utils; | ||
2 | |||
3 | import java.io.BufferedInputStream; | ||
4 | import java.io.ByteArrayOutputStream; | ||
5 | import java.io.IOException; | ||
6 | import java.net.URLConnection; | ||
7 | |||
8 | /** | ||
9 | * A utility class for making HTTP requests. | ||
10 | * | ||
11 | * @author Marko Zajc | ||
12 | */ | ||
13 | public class HTTPUtils { | ||
14 | |||
15 | private HTTPUtils() {} | ||
16 | |||
17 | /** | ||
18 | * Reads {@link URLConnection} into a byte array. | ||
19 | * | ||
20 | * @param conn | ||
21 | * connection to read from | ||
22 | * | ||
23 | * @return content as a byte array | ||
24 | * @throws IOException | ||
25 | * @see String#String(byte[], String) | ||
26 | */ | ||
27 | public static byte[] read(URLConnection conn) throws IOException { | ||
28 | try (BufferedInputStream is = new BufferedInputStream(conn.getInputStream())) { | ||
29 | ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); | ||
30 | |||
31 | byte[] chunk = new byte[4096]; | ||
32 | int bytesRead; | ||
33 | |||
34 | while ((bytesRead = is.read(chunk)) > -1) | ||
35 | outputStream.write(chunk, 0, bytesRead); | ||
36 | |||
37 | return outputStream.toByteArray(); | ||
38 | } | ||
39 | } | ||
40 | |||
41 | } | ||
diff --git a/src/main/java/com/markozajc/akiwrapper/core/utils/JSONUtils.java b/src/main/java/com/markozajc/akiwrapper/core/utils/JSONUtils.java index 69919ab..e2dbe27 100644 --- a/src/main/java/com/markozajc/akiwrapper/core/utils/JSONUtils.java +++ b/src/main/java/com/markozajc/akiwrapper/core/utils/JSONUtils.java | |||
@@ -1,5 +1,10 @@ | |||
1 | package com.markozajc.akiwrapper.core.utils; | 1 | package com.markozajc.akiwrapper.core.utils; |
2 | 2 | ||
3 | import java.util.Optional; | ||
4 | |||
5 | import javax.annotation.Nonnull; | ||
6 | |||
7 | import org.json.JSONException; | ||
3 | import org.json.JSONObject; | 8 | import org.json.JSONObject; |
4 | 9 | ||
5 | /** | 10 | /** |
@@ -7,64 +12,88 @@ import org.json.JSONObject; | |||
7 | * | 12 | * |
8 | * @author Marko Zajc | 13 | * @author Marko Zajc |
9 | */ | 14 | */ |
10 | public class JSONUtils { | 15 | public final class JSONUtils { |
11 | 16 | ||
12 | private JSONUtils() {} | 17 | private JSONUtils() {} |
13 | 18 | ||
14 | /** | 19 | /** |
20 | * Gets an {@link Integer} value from a {@link JSONObject}. | ||
21 | * | ||
15 | * @param json | 22 | * @param json |
16 | * @param key | 23 | * @param key |
17 | * @return value from that key as an integer | 24 | * |
18 | * @throws NumberFormatException | 25 | * @return {@link Optional} integer value |
19 | * if the value could in no way be transferred to an integer | ||
20 | */ | 26 | */ |
21 | public static Integer getInteger(JSONObject json, String key) { | 27 | @SuppressWarnings("null") |
22 | Object object = json.get(key); | 28 | @Nonnull |
23 | 29 | public static Optional<Integer> getInteger(@Nonnull JSONObject json, @Nonnull String key) { | |
24 | if (object == null) | 30 | try { |
25 | return null; | 31 | Object object = json.get(key); |
32 | Integer value; | ||
33 | if (object instanceof Number) | ||
34 | value = Integer.valueOf(((Number) object).intValue()); | ||
35 | else if (object instanceof String) | ||
36 | value = Integer.valueOf((String) object); | ||
37 | else | ||
38 | throw new NumberFormatException("Could not format \"" + object + | ||
39 | "\" of type " + | ||
40 | object.getClass().getName() + | ||
41 | " into a Double."); | ||
26 | 42 | ||
27 | if (object instanceof Number) | 43 | return Optional.of(value); |
28 | return Integer.valueOf(((Number) object).intValue()); | ||
29 | 44 | ||
30 | if (object instanceof String) | 45 | } catch (JSONException e) { // NOSONAR It just means that the key wasn't found. |
31 | return Integer.valueOf((String) object); | 46 | return Optional.empty(); |
32 | 47 | } | |
33 | throw new NumberFormatException( | ||
34 | "Could not format \"" + object + "\" of type " + object.getClass().getName() + " into an Integer."); | ||
35 | } | 48 | } |
36 | 49 | ||
37 | /** | 50 | /** |
51 | * Gets a {@link Double} value from a {@link JSONObject}. | ||
52 | * | ||
38 | * @param json | 53 | * @param json |
39 | * @param key | 54 | * @param key |
40 | * @return value from that key as a double | 55 | * |
41 | * @throws NumberFormatException | 56 | * @return {@link Optional} double value |
42 | * if the value could in no way be transferred to a double | ||
43 | */ | 57 | */ |
44 | public static Double getDouble(JSONObject json, String key) { | 58 | @SuppressWarnings("null") |
45 | Object object = json.get(key); | 59 | @Nonnull |
46 | 60 | public static Optional<Double> getDouble(@Nonnull JSONObject json, @Nonnull String key) { | |
47 | if (object == null) | 61 | try { |
48 | return null; | 62 | Object object = json.get(key); |
49 | 63 | Double value; | |
50 | if (object instanceof Number) | 64 | if (object instanceof Number) |
51 | return Double.valueOf(((Number) object).doubleValue()); | 65 | value = Double.valueOf(((Number) object).doubleValue()); |
66 | else if (object instanceof String) | ||
67 | value = Double.valueOf((String) object); | ||
68 | else | ||
69 | throw new NumberFormatException("Could not format \"" + object + | ||
70 | "\" of type " + | ||
71 | object.getClass().getName() + | ||
72 | " into a Double."); | ||
52 | 73 | ||
53 | if (object instanceof String) | 74 | return Optional.of(value); |
54 | return Double.valueOf((String) object); | ||
55 | 75 | ||
56 | throw new NumberFormatException( | 76 | } catch (JSONException e) { // NOSONAR It just means that the key wasn't found. |
57 | "Could not format \"" + object + "\" of type " + object.getClass().getName() + " into a Double."); | 77 | return Optional.empty(); |
78 | } | ||
58 | } | 79 | } |
59 | 80 | ||
60 | /** | 81 | /** |
82 | * Gets a {@link String} value from a {@link JSONObject}. | ||
83 | * | ||
61 | * @param json | 84 | * @param json |
62 | * @param key | 85 | * @param key |
63 | * @return value from that key as a string (calls {@link Object#toString()} on | 86 | * |
64 | * the object) | 87 | * @return {@link Optional} string value |
65 | */ | 88 | */ |
66 | public static String getString(JSONObject json, String key) { | 89 | @SuppressWarnings("null") |
67 | return json.get(key).toString(); | 90 | @Nonnull |
91 | public static Optional<String> getString(@Nonnull JSONObject json, @Nonnull String key) { | ||
92 | try { | ||
93 | return Optional.of(json.get(key).toString()); | ||
94 | } catch (JSONException e) { // NOSONAR It just means that the key wasn't found. | ||
95 | return Optional.empty(); | ||
96 | } | ||
68 | } | 97 | } |
69 | 98 | ||
70 | } | 99 | } |
diff --git a/src/main/java/com/markozajc/akiwrapper/core/utils/Servers.java b/src/main/java/com/markozajc/akiwrapper/core/utils/Servers.java index 14ebedc..3aa0fc2 100644 --- a/src/main/java/com/markozajc/akiwrapper/core/utils/Servers.java +++ b/src/main/java/com/markozajc/akiwrapper/core/utils/Servers.java | |||
@@ -1,30 +1,20 @@ | |||
1 | package com.markozajc.akiwrapper.core.utils; | 1 | package com.markozajc.akiwrapper.core.utils; |
2 | 2 | ||
3 | import java.io.IOException; | ||
4 | import java.io.InputStream; | ||
5 | import java.util.ArrayList; | ||
6 | import java.util.Collections; | ||
7 | import java.util.EnumMap; | ||
8 | import java.util.List; | 3 | import java.util.List; |
9 | import java.util.Map; | 4 | import java.util.stream.Collectors; |
10 | import java.util.Scanner; | 5 | import java.util.stream.Stream; |
11 | import java.util.logging.Logger; | ||
12 | 6 | ||
13 | import javax.annotation.Nonnull; | 7 | import javax.annotation.Nonnull; |
14 | 8 | ||
15 | import org.json.JSONObject; | 9 | import com.jcabi.xml.XMLDocument; |
16 | |||
17 | import com.markozajc.akiwrapper.core.Route; | 10 | import com.markozajc.akiwrapper.core.Route; |
18 | import com.markozajc.akiwrapper.core.entities.AkiwrapperMetadata; | ||
19 | import com.markozajc.akiwrapper.core.entities.Server; | 11 | import com.markozajc.akiwrapper.core.entities.Server; |
12 | import com.markozajc.akiwrapper.core.entities.Server.GuessType; | ||
20 | import com.markozajc.akiwrapper.core.entities.Server.Language; | 13 | import com.markozajc.akiwrapper.core.entities.Server.Language; |
21 | import com.markozajc.akiwrapper.core.entities.ServerGroup; | 14 | import com.markozajc.akiwrapper.core.entities.ServerList; |
22 | import com.markozajc.akiwrapper.core.entities.Status.Level; | ||
23 | import com.markozajc.akiwrapper.core.entities.impl.immutable.ServerGroupImpl; | ||
24 | import com.markozajc.akiwrapper.core.entities.impl.immutable.ServerImpl; | 15 | import com.markozajc.akiwrapper.core.entities.impl.immutable.ServerImpl; |
25 | import com.markozajc.akiwrapper.core.entities.impl.immutable.StatusImpl; | 16 | import com.markozajc.akiwrapper.core.entities.impl.immutable.ServerListImpl; |
26 | import com.markozajc.akiwrapper.core.exceptions.ServerGroupUnavailableException; | 17 | import com.markozajc.akiwrapper.core.exceptions.ServerNotFoundException; |
27 | import com.markozajc.akiwrapper.core.exceptions.StatusException; | ||
28 | 18 | ||
29 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; | 19 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; |
30 | 20 | ||
@@ -33,147 +23,55 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; | |||
33 | * | 23 | * |
34 | * @author Marko Zajc | 24 | * @author Marko Zajc |
35 | */ | 25 | */ |
36 | @SuppressWarnings("null") | ||
37 | @SuppressFBWarnings("REC_CATCH_EXCEPTION") | 26 | @SuppressFBWarnings("REC_CATCH_EXCEPTION") |
38 | public class Servers { | 27 | public final class Servers { |
39 | 28 | ||
40 | private static final int MAX_SCRAP_ATTEMPTS = 3; | 29 | private static final String FOOTPRINT = "cd8e6509f3420878e18d75b9831b317f"; |
41 | /** | 30 | private static final String LIST_URL = |
42 | * The format for Akinator's base URL format. Use with | 31 | "https://global3.akinator.com/ws/instances_v2.php?media_id=14" + "&mode=https&footprint=" + FOOTPRINT; |
43 | * {@link String#format(String, Object...)} and provide the hostname and the port as | ||
44 | * the parameter (for example {@code srv1.akinator.com.9100}}. | ||
45 | */ | ||
46 | public static final String BASE_URL_FORMAT = "https://%s/ws/"; | ||
47 | 32 | ||
48 | private Servers() {} | 33 | private Servers() {} |
49 | 34 | ||
50 | /** | 35 | /** |
51 | * A list of all known Akinator's API servers. | 36 | * Finds correct {@link Server}s using given parameters and compiles a |
52 | */ | 37 | * {@link ServerList} out of them. |
53 | public static final Map<Language, ServerGroup> SERVER_GROUPS; | ||
54 | |||
55 | static { | ||
56 | |||
57 | Map<Language, ServerGroup> serverGroups = new EnumMap<>(Language.class); | ||
58 | |||
59 | try { | ||
60 | Map<Language, List<Server>> servers = new EnumMap<>(Language.class); | ||
61 | |||
62 | try (InputStream is = Servers.class.getResourceAsStream("/servers.json")) { | ||
63 | try (Scanner s = new Scanner(is, "UTF-8")) { | ||
64 | s.useDelimiter("\\A"); | ||
65 | |||
66 | JSONObject serversBaseJson = new JSONObject(s.hasNext() ? s.next() : "{}"); | ||
67 | |||
68 | if (!serversBaseJson.has("servers")) | ||
69 | throw new IOException(); | ||
70 | |||
71 | serversBaseJson.getJSONArray("servers").forEach(o -> { | ||
72 | JSONObject serverJson = (JSONObject) o; | ||
73 | Language localization = Language.valueOf(serverJson.getString("localization")); | ||
74 | |||
75 | if (!servers.containsKey(localization)) | ||
76 | servers.put(localization, new ArrayList<>()); | ||
77 | |||
78 | servers.get(localization).add(new ServerImpl(serverJson.getString("host"), localization)); | ||
79 | }); | ||
80 | |||
81 | } | ||
82 | } | ||
83 | |||
84 | servers.forEach((language, server) -> serverGroups.put(language, new ServerGroupImpl(language, server))); | ||
85 | |||
86 | } catch (Exception e) { | ||
87 | System.err.println("[ERROR] Akiwrapper - Couldn't load the server list; " + e); // NOSONAR | ||
88 | } | ||
89 | |||
90 | SERVER_GROUPS = Collections.unmodifiableMap(serverGroups); | ||
91 | } | ||
92 | |||
93 | /** | ||
94 | * Checks if an API server is online. | ||
95 | * | 38 | * |
96 | * @param server | 39 | * @param localization |
97 | * a server to check | 40 | * language of the server to search for |
98 | * @return true if a new session can be created on the provided server, false if not | 41 | * @param guessType |
99 | */ | 42 | * guessType of the server to search for |
100 | public static boolean isUp(Server server) { | ||
101 | return isUp(server, 0); | ||
102 | } | ||
103 | |||
104 | /** | ||
105 | * Checks if an API server is online. | ||
106 | * | 43 | * |
107 | * @param server | 44 | * @return a {@link ServerList} with {@link Server}s that suit the given parameters. |
108 | * a server to check | 45 | * |
109 | * @return true if a new session can be created on the provided server, false if not | 46 | * @throws ServerNotFoundException |
47 | * if there is no server that matches the query. | ||
110 | */ | 48 | */ |
111 | private static boolean isUp(Server server, int attempt) { | 49 | @Nonnull |
112 | try { | 50 | public static ServerList findServers(@Nonnull Language localization, |
113 | JSONObject question = Route.NEW_SESSION | 51 | @Nonnull GuessType guessType) throws ServerNotFoundException { |
114 | .getRequest(server.getApiUrl(), true, AkiwrapperMetadata.DEFAULT_NAME) | 52 | List<Server> servers = getServers().filter(s -> s.getGuessType() == guessType) |
115 | .getJSON(true); | 53 | .filter(s -> s.getLanguage() == localization) |
116 | // Checks if a server can be connected to by creating a new session on it | 54 | .collect(Collectors.toList()); |
117 | 55 | if (servers.isEmpty()) | |
118 | if (new StatusImpl(question).getLevel().equals(Level.OK)) | 56 | throw new ServerNotFoundException(); |
119 | return true; | 57 | return new ServerListImpl(servers); |
120 | |||
121 | } catch (StatusException e) { | ||
122 | if (e.getStatus().getReason().startsWith("KEY NOT FOUND")) { | ||
123 | // Checks if the exception was thrown because of an obsolete API key | ||
124 | |||
125 | if (attempt > MAX_SCRAP_ATTEMPTS) | ||
126 | return false; | ||
127 | // In case something goes terribly wrong and the API key does not get scraped, but | ||
128 | // neither is an exception thrown on the Route.scrapApiKey call - or the KNF error | ||
129 | // has been returned for a reason not connected with the API key | ||
130 | |||
131 | try { | ||
132 | Route.accquireApiKey(); | ||
133 | return isUp(server, attempt + 1); | ||
134 | // Attempts to "rescrap" the API key and run the method again | ||
135 | |||
136 | } catch (IOException ioe) { | ||
137 | // In case API key can not be scraped. If this ever occurs, it's Akiwrapper's fault | ||
138 | // (or you haven't updated to the newest version) | ||
139 | Logger.getLogger("Akiwrapper").severe("Couldn't scrape the API key; " + ioe.toString()); | ||
140 | return false; | ||
141 | } | ||
142 | |||
143 | } | ||
144 | |||
145 | } catch (IllegalArgumentException | IOException e) { | ||
146 | // If the server is unreachable | ||
147 | } | ||
148 | |||
149 | return false; | ||
150 | } | 58 | } |
151 | 59 | ||
152 | /** | 60 | /** |
153 | * Searches for an available server for the given localization language. If there is | 61 | * Fetches and builds a {@link Stream} of {@link Server}s from the server-listing API |
154 | * no available server, this will throw a {@link ServerGroupUnavailableException}. | 62 | * endpoint. All servers in this list should be up and running. |
155 | * | 63 | * |
156 | * @param localization | 64 | * @return a {@link Stream} of all {@link Server}s. |
157 | * language of the server to search for | ||
158 | * @return the first server available for that language | ||
159 | * @throws UnsupportedOperationException | ||
160 | * if language {@code localization} is not supported by the Akinator's | ||
161 | * API | ||
162 | * @throws ServerGroupUnavailableException | ||
163 | * if there are no available servers for language {@code localization} | ||
164 | */ | 65 | */ |
165 | @Nonnull | 66 | @SuppressWarnings("null") |
166 | public static Server getFirstAvailableServer(@Nonnull Language localization) { | 67 | public static Stream<Server> getServers() { |
167 | ServerGroup sg = SERVER_GROUPS.get(localization); | 68 | return new XMLDocument(fetchListXml()).nodes("//RESULT/PARAMETERS/*") |
168 | if (sg == null) | 69 | .stream() |
169 | throw new IllegalArgumentException( | 70 | .flatMap(xml -> ServerImpl.fromXml(xml).stream()); |
170 | "Language " + localization.toString() + " is not supported by the Akinator's API."); | 71 | } |
171 | |||
172 | Server result = sg.getFirstAvailableServer(); | ||
173 | if (result == null) | ||
174 | throw new ServerGroupUnavailableException(sg); | ||
175 | 72 | ||
176 | return result; | 73 | private static String fetchListXml() { |
74 | return Route.UNIREST.get(LIST_URL).asString().getBody(); | ||
177 | } | 75 | } |
178 | 76 | ||
179 | } | 77 | } |
diff --git a/src/main/java/com/markozajc/akiwrapper/listbuilder/AkinatorServerScanner.java b/src/main/java/com/markozajc/akiwrapper/listbuilder/AkinatorServerScanner.java deleted file mode 100644 index cf73355..0000000 --- a/src/main/java/com/markozajc/akiwrapper/listbuilder/AkinatorServerScanner.java +++ /dev/null | |||
@@ -1,424 +0,0 @@ | |||
1 | /* | ||
2 | * Read before using: | ||
3 | * ================================================================================== | ||
4 | * ==================================[ DISCLAIMER ]================================== | ||
5 | * ================================================================================== | ||
6 | * | ||
7 | * This might (rarely) be considered a port scanning tool by some ISPs if incorrectly | ||
8 | * configured or not (even though it only scans a small range of ports). Get familiar | ||
9 | * with your ISP's policies before using it! | ||
10 | * | ||
11 | * I assume no liability whatsoever for any damage, direct or indirect, you getting | ||
12 | * kicked offline by your ISP or any nuclear wars, apocalypse and so on caused by | ||
13 | * this software. | ||
14 | * | ||
15 | * (also it might break your router so that you'll have to restart it if you set the | ||
16 | * THREAD_POOL_SIZE value too high, but it's nothing severe) | ||
17 | * | ||
18 | * (also my sincerest apologies for every "/.../ a ENGLISH /.../" you see while using | ||
19 | * this!) | ||
20 | * | ||
21 | * With that out of the way, you're free to tweak and use this piece of software to | ||
22 | * any extent. | ||
23 | */ | ||
24 | package com.markozajc.akiwrapper.listbuilder; | ||
25 | |||
26 | import java.io.FileNotFoundException; | ||
27 | import java.io.IOException; | ||
28 | import java.io.PrintWriter; | ||
29 | import java.io.UnsupportedEncodingException; | ||
30 | import java.net.ConnectException; | ||
31 | import java.net.InetSocketAddress; | ||
32 | import java.net.Socket; | ||
33 | import java.net.SocketTimeoutException; | ||
34 | import java.net.UnknownHostException; | ||
35 | import java.util.ArrayList; | ||
36 | import java.util.Collections; | ||
37 | import java.util.HashMap; | ||
38 | import java.util.List; | ||
39 | import java.util.Map; | ||
40 | import java.util.Objects; | ||
41 | import java.util.Random; | ||
42 | import java.util.concurrent.ExecutionException; | ||
43 | import java.util.concurrent.ExecutorService; | ||
44 | import java.util.concurrent.Executors; | ||
45 | import java.util.concurrent.Future; | ||
46 | import java.util.concurrent.atomic.AtomicInteger; | ||
47 | import java.util.stream.Collectors; | ||
48 | |||
49 | import org.json.JSONArray; | ||
50 | import org.json.JSONException; | ||
51 | import org.json.JSONObject; | ||
52 | |||
53 | import com.markozajc.akiwrapper.core.Route; | ||
54 | import com.markozajc.akiwrapper.core.Route.Request; | ||
55 | import com.markozajc.akiwrapper.core.entities.Server; | ||
56 | import com.markozajc.akiwrapper.core.entities.Server.Language; | ||
57 | import com.markozajc.akiwrapper.core.entities.impl.immutable.ServerImpl; | ||
58 | import com.markozajc.akiwrapper.core.exceptions.ServerUnavailableException; | ||
59 | import com.markozajc.akiwrapper.core.exceptions.StatusException; | ||
60 | import com.markozajc.akiwrapper.core.utils.Servers; | ||
61 | |||
62 | /** | ||
63 | * A class used to build lists of available {@link Server}s along with their | ||
64 | * {@link Language}s. Currently <u>highly experimental</u>. <b>USE ONLY IF YOU HAVE | ||
65 | * READ THE DISCLAIMER IN CLASS'S HEADER!!<b> | ||
66 | * | ||
67 | * @author Marko Zajc | ||
68 | */ | ||
69 | public class AkinatorServerScanner { | ||
70 | |||
71 | // =========================================== | ||
72 | // Configuration | ||
73 | // =========================================== | ||
74 | |||
75 | private static final long STATUS_INTERVAL = 3000; | ||
76 | // Interval at which the status message should be sent (in milliseconds) | ||
77 | |||
78 | private static final String DUMP_FILENAME_FORMAT = "apidumpd{ts}.json"; | ||
79 | // Format of the server dump file. "{ts}" is replaced with the current timestamp. | ||
80 | |||
81 | private static final int FAILURE_COMBO_TOLERANCE = 5; | ||
82 | // How many UnknownHostExceptions can be hit before the host scanner aborts | ||
83 | |||
84 | private static final boolean DEBUG_OUTPUT = true; | ||
85 | // Whether to output debug information | ||
86 | |||
87 | private static final int CONNECTION_TIMEOUT = 150; | ||
88 | // Connection timeout. Lower values mean faster but less accurate, higher mean the | ||
89 | // opposite. Decreasing this below 70 might go godspeed, but rest assured that | ||
90 | // results will be a lot less accurate that way. | ||
91 | // Decrease this to get more accurate results (slower, more accurate). | ||
92 | // Increase this to allow for better multi-threading (faster, less accurate). | ||
93 | |||
94 | private static final int THREAD_POOL_SIZE = 1; | ||
95 | // Thread poll size of the server scanner ExecutorService fixed thread pool. | ||
96 | // Increasing this to something too high might crash your JVM, your router (no joke), | ||
97 | // or get you kicked offline by your ISP (again, not kidding as it might get a bit | ||
98 | // spammy and thus considered an abuse by your ISP). Increasing it above 50 might | ||
99 | // have unwanted side effects. | ||
100 | // Decrease this to maybe get more accurate results (slower, more accurate). | ||
101 | // Increase this to allow for better multi-threading (faster, less accurate). | ||
102 | |||
103 | private static final boolean IGNORE_PING_FAILS = false; | ||
104 | // Whether to still scan the hosts that returned errors on a ping (EXPERIMENTAL, | ||
105 | // turning it on will usually just prolong the scan time). | ||
106 | |||
107 | // =========================================== | ||
108 | // Messages format | ||
109 | // =========================================== | ||
110 | |||
111 | // You may modify these, but adding "%s" tokens might throw a | ||
112 | // java.util.MissingFormatArgumentException at runtime! | ||
113 | |||
114 | private static final String HOST_EXISTS_BUT_TIMEOUTS = "[WARN] Host %s exists, but appears to be unavailable. Excluding it from the API scan.\n"; | ||
115 | private static final String HOST_CANT_PING = "[ERORR] Couldn't ping host %s; %s.\n"; | ||
116 | private static final String HOST_LISTING = "[INFO] Listing API hosts.\n"; | ||
117 | private static final String HOST_LISTED = "[INFO] Listed %s API hosts. Took %s milliseconds.\n"; | ||
118 | private static final String HOST_VERIFIED = "[DEBUG] Host %s is most likely an API server.\n"; | ||
119 | private static final String HOST_CANT_CONNECT = "[DEBUG] Can't connect to host %s, most likely due to it blocking connections on port 80.\n"; | ||
120 | |||
121 | private static final String API_LISTING = "[INFO] Initializing the localized API services scan (roughly %s ports).\n"; | ||
122 | private static final String API_PORT_QUERYING = "[DEBUG] Submitting a search for a localized API service @ %s to the ExecutorService.\n"; | ||
123 | private static final String API_SCAN_BEGIN = "[INFO] Beginning the API service scan.\n"; | ||
124 | private static final String API_EXECUTION_EXCEPTION = "[ERROR] Failed to retrieve the API call response; %s.\n"; | ||
125 | private static final String API_INTERRUPTED = "[DEBUG] Got interrupted while fetching the API call response.\n"; | ||
126 | |||
127 | private static final String SERVER_HIT = "[INFO] HIT! %s seems to be a %s Akinator API server.\n"; | ||
128 | private static final String SERVER_TIMEOUT = "[DEBUG] %s timeouts.\n"; | ||
129 | private static final String SERVER_JSON_ERROR = "[ERROR] %s - server is reachable, but returned an invalid JSON response.\n"; | ||
130 | private static final String SERVER_INTERNAL_ERROR = "[ERROR] %s - server is reachable, but reports an internal error; %s.\n"; | ||
131 | private static final String SERVER_DOWN = "[ERROR] %s - server is reachable, its localization can't be tested due to it being down.\n"; | ||
132 | private static final String SERVER_NO_LANGUAGE = "[ERROR] %s - server is reachable, but returns an unknown localization (%s @ index %s).\n"; | ||
133 | private static final String SERVER_CANT_CONNECT = "[ERROR] Connection with %s can't be established; %s.\n"; | ||
134 | |||
135 | private static final String SERIALIZING = "[INFO] Serializing %s found API servers...\n"; | ||
136 | |||
137 | private static final String FILE_CANT_DUMP = "[ERROR] Couldn't dump into %s (%s). %s API servers have been dumped into stout (as JSON)!\n"; | ||
138 | private static final String FILE_COMPLETE = "=======================================================\n" | ||
139 | + "[INFO] Done! Dumped %s API servers into %s (as JSON)!\n" | ||
140 | + "==========================================================\n"; | ||
141 | |||
142 | private static final String STATUS = "[INFO] Scanning ports.. %s/%s (%s%%)\n"; | ||
143 | |||
144 | //@formatter:off///////////////////////////////////////////////////////////////////////////////////////////////////// | ||
145 | /////////////////////! DO NOT EDIT STUFF BEYOND THIS LINE (unless you know what you're doing) ! ///////////////////// | ||
146 | ////////////////////////////////////////////////////////////////////////////////////////////////////////@formatter:on | ||
147 | |||
148 | private static final String API_FORMAT = "%s:%s"; | ||
149 | // Don't change this, defined by TCP/IP | ||
150 | |||
151 | private static final String HOSTNAME_FORMAT = "srv%s.akinator.com"; | ||
152 | private static final int API_PORT_MIN = 9000; | ||
153 | private static final int API_PORT_MAX = 9300; | ||
154 | // Usually no need to change these | ||
155 | |||
156 | private static final int ANSWER_INDEX = 2; | ||
157 | // Currently the index for the "Don't know" answer, DON'T CHANGE UNLESS YOU CHANGE | ||
158 | // THE STATIC CONSTRUCTOR INITIALIZATION OF "ANSWER_MAPPINGS" AS WELL!! | ||
159 | |||
160 | private static int totalPorts = 0; | ||
161 | // Altered at runtime, changing it will have no effect | ||
162 | |||
163 | private static final Random RANDOM = new Random(); | ||
164 | // Used only for probe name generation | ||
165 | |||
166 | private static final Map<String, Language> ANSWER_MAPPINGS; | ||
167 | // Set in the static constructor | ||
168 | |||
169 | private static final String PROBE_NAME; | ||
170 | // Set in the static constructor | ||
171 | |||
172 | private static final AtomicInteger REMAINING_PORTS; | ||
173 | // Set in the static constructor | ||
174 | |||
175 | private static final ExecutorService SERVER_SCANNER_ES; | ||
176 | // Set in the static constructor | ||
177 | |||
178 | private static final Thread STATUS_THREAD; | ||
179 | // Set in the static constructor | ||
180 | |||
181 | private static final Route NEW_SESSION; | ||
182 | // Set in the static constructor | ||
183 | |||
184 | static { | ||
185 | REMAINING_PORTS = new AtomicInteger(); | ||
186 | PROBE_NAME = "AkiwrapperProbe" + RANDOM.nextInt(); | ||
187 | |||
188 | Map<String, Language> answerMappings = new HashMap<>(); | ||
189 | |||
190 | answerMappings.put("\u0627\u0646\u0627 \u0644\u0627 \u0627\u0639\u0644\u0645", Language.ARABIC); | ||
191 | answerMappings.put("\u4e0d\u77e5\u9053", Language.CHINESE); | ||
192 | answerMappings.put("Weet ik niet", Language.DUTCH); | ||
193 | answerMappings.put("Don't know", Language.ENGLISH); | ||
194 | answerMappings.put("Ne sais pas", Language.FRENCH); | ||
195 | answerMappings.put("Ich wei\u00df nicht", Language.GERMAN); | ||
196 | answerMappings.put("\u05d0\u05e0\u05d9 \u05dc\u05d0 \u05d9\u05d5\u05d3\u05e2", Language.HEBREW); | ||
197 | answerMappings.put("Non lo so", Language.ITALIAN); | ||
198 | answerMappings.put("\u5206\u304b\u3089\u306a\u3044", Language.JAPANESE); | ||
199 | answerMappings.put("\ubaa8\ub974\uaca0\uc2b5\ub2c8\ub2e4", Language.KOREAN); | ||
200 | answerMappings.put("Nie wiem", Language.POLISH); | ||
201 | answerMappings.put("N\u00e3o sei", Language.PORTUGUESE); | ||
202 | answerMappings.put("\u042f \u043d\u0435 \u0437\u043d\u0430\u044e", Language.RUSSIAN); | ||
203 | answerMappings.put("No lo s\u00e9", Language.SPANISH); | ||
204 | answerMappings.put("Bilmiyorum", Language.TURKISH); | ||
205 | answerMappings.put("Tidak tahu", Language.MALAY); | ||
206 | // Escaped non-ASCII characters for better platform encoding independence | ||
207 | |||
208 | Request.connectionTimeout = CONNECTION_TIMEOUT; | ||
209 | NEW_SESSION = Route.NEW_SESSION.setUserAgent(PROBE_NAME); | ||
210 | |||
211 | ANSWER_MAPPINGS = Collections.unmodifiableMap(answerMappings); | ||
212 | SERVER_SCANNER_ES = Executors.newFixedThreadPool(THREAD_POOL_SIZE, r -> { | ||
213 | |||
214 | Thread t = new Thread(r, "server-scanner"); | ||
215 | t.setDaemon(true); | ||
216 | return t; | ||
217 | |||
218 | }); | ||
219 | STATUS_THREAD = new Thread(() -> { | ||
220 | |||
221 | while (true) { | ||
222 | reportStatus(totalPorts - REMAINING_PORTS.get()); | ||
223 | |||
224 | try { | ||
225 | Thread.sleep(STATUS_INTERVAL); | ||
226 | } catch (InterruptedException e) { | ||
227 | Thread.currentThread().interrupt(); | ||
228 | } | ||
229 | } | ||
230 | |||
231 | }, "status-thread"); | ||
232 | STATUS_THREAD.setDaemon(true); | ||
233 | } | ||
234 | |||
235 | /** | ||
236 | * Runs the {@link AkinatorServerScanner}. | ||
237 | * | ||
238 | * @param args | ||
239 | * does nothing. The configuration parameters are the first few constants | ||
240 | * (you'll know what they do if you'll read the comments below them) | ||
241 | */ | ||
242 | public static void main(String[] args) { | ||
243 | |||
244 | System.out.printf(HOST_LISTING); | ||
245 | long start = System.currentTimeMillis(); | ||
246 | List<String> hostnames = listHosts(); | ||
247 | |||
248 | System.out.printf(HOST_LISTED, hostnames.size(), System.currentTimeMillis() - start); | ||
249 | int remaining = hostnames.size() * (API_PORT_MAX - API_PORT_MIN); | ||
250 | |||
251 | System.out.printf(API_LISTING, remaining); | ||
252 | totalPorts = remaining; | ||
253 | REMAINING_PORTS.set(remaining); | ||
254 | List<Server> servers = scanHosts(hostnames); | ||
255 | |||
256 | System.out.printf(SERIALIZING, servers.size()); | ||
257 | String serialized = serializeServers(servers); | ||
258 | |||
259 | String filename = DUMP_FILENAME_FORMAT.replace("{ts}", Long.toString(System.currentTimeMillis())); | ||
260 | |||
261 | try (PrintWriter writer = new PrintWriter(filename, "UTF-8")) { | ||
262 | writer.print(serialized); | ||
263 | } catch (FileNotFoundException e) { | ||
264 | System.err.printf(FILE_CANT_DUMP, filename, e, servers.size()); | ||
265 | |||
266 | } catch (UnsupportedEncodingException e) { | ||
267 | // Can't happen, JVM always supports UTF-8 | ||
268 | } | ||
269 | |||
270 | System.out.printf(FILE_COMPLETE, servers.size(), filename); | ||
271 | } | ||
272 | |||
273 | private static void isAvailable(String host, int timeout) throws IOException { | ||
274 | try (Socket socket = new Socket()) { | ||
275 | try { | ||
276 | socket.connect(new InetSocketAddress(host, 80 /* not all servers support HTTPS! */), timeout); | ||
277 | } catch (ConnectException e) { | ||
278 | if (DEBUG_OUTPUT) { | ||
279 | System.out.printf(HOST_CANT_CONNECT, host); | ||
280 | } | ||
281 | } | ||
282 | } | ||
283 | } | ||
284 | |||
285 | private static List<String> listHosts() { | ||
286 | List<String> result = new ArrayList<>(); | ||
287 | int failCombo = 0; | ||
288 | for (int i = 1; failCombo <= FAILURE_COMBO_TOLERANCE; i++) { | ||
289 | String hostname = String.format(HOSTNAME_FORMAT, i); | ||
290 | try { | ||
291 | isAvailable(hostname, 3000); | ||
292 | result.add(hostname); | ||
293 | failCombo = 0; | ||
294 | |||
295 | if (DEBUG_OUTPUT) | ||
296 | System.out.printf(HOST_VERIFIED, hostname); | ||
297 | |||
298 | } catch (SocketTimeoutException e) { | ||
299 | System.err.printf(HOST_EXISTS_BUT_TIMEOUTS, hostname); | ||
300 | |||
301 | if (IGNORE_PING_FAILS) | ||
302 | result.add(hostname); | ||
303 | |||
304 | } catch (UnknownHostException e) { | ||
305 | failCombo++; | ||
306 | |||
307 | } catch (IOException e) { | ||
308 | failCombo++; | ||
309 | |||
310 | System.err.printf(HOST_CANT_PING, hostname, e); | ||
311 | } | ||
312 | } | ||
313 | |||
314 | return result; | ||
315 | } | ||
316 | |||
317 | private static List<Server> scanHosts(List<String> hostnames) { | ||
318 | |||
319 | List<Future<Server>> serversCombined = new ArrayList<>(); | ||
320 | for (String hostname : hostnames) | ||
321 | serversCombined.addAll(scanHost(hostname)); | ||
322 | |||
323 | System.out.printf(API_SCAN_BEGIN); | ||
324 | STATUS_THREAD.start(); | ||
325 | |||
326 | return serversCombined.stream().map(t -> { | ||
327 | try { | ||
328 | return t.get(); | ||
329 | } catch (InterruptedException e) { | ||
330 | if (DEBUG_OUTPUT) | ||
331 | System.out.printf(API_INTERRUPTED); | ||
332 | |||
333 | Thread.currentThread().interrupt(); | ||
334 | |||
335 | } catch (ExecutionException e) { | ||
336 | System.err.printf(API_EXECUTION_EXCEPTION, e); | ||
337 | } | ||
338 | return null; | ||
339 | }).filter(Objects::nonNull).collect(Collectors.toList()); | ||
340 | } | ||
341 | |||
342 | private static List<Future<Server>> scanHost(String hostname) { | ||
343 | List<Future<Server>> futures = new ArrayList<>(); | ||
344 | |||
345 | for (int i = 0; i < API_PORT_MAX - API_PORT_MIN; i++) { | ||
346 | int port = i + API_PORT_MIN; | ||
347 | if (DEBUG_OUTPUT) | ||
348 | System.out.printf(API_PORT_QUERYING, port); | ||
349 | |||
350 | futures.add(SERVER_SCANNER_ES.submit(() -> scanServer(hostname, port))); | ||
351 | } | ||
352 | |||
353 | return futures; | ||
354 | } | ||
355 | |||
356 | @SuppressWarnings("null") | ||
357 | private static Server scanServer(String hostname, int port) { | ||
358 | String base = String.format(API_FORMAT, hostname, port); | ||
359 | |||
360 | try { | ||
361 | String answer = NEW_SESSION.getRequest(String.format(Servers.BASE_URL_FORMAT, base), false, PROBE_NAME) | ||
362 | .getJSON() | ||
363 | .getJSONObject("parameters") | ||
364 | .getJSONObject("step_information") | ||
365 | .getJSONArray("answers") | ||
366 | .getJSONObject(ANSWER_INDEX) | ||
367 | .getString("answer"); | ||
368 | |||
369 | Language localization = ANSWER_MAPPINGS.get(answer); | ||
370 | if (localization == null) { | ||
371 | System.err.printf(SERVER_NO_LANGUAGE, base, answer, ANSWER_INDEX); | ||
372 | |||
373 | } else { | ||
374 | System.out.printf(SERVER_HIT, base, localization); | ||
375 | return new ServerImpl(base, localization); | ||
376 | } | ||
377 | |||
378 | } catch (JSONException e) { | ||
379 | System.err.printf(SERVER_JSON_ERROR, base); | ||
380 | |||
381 | } catch (SocketTimeoutException e) { | ||
382 | if (DEBUG_OUTPUT) | ||
383 | System.out.printf(SERVER_TIMEOUT, base); | ||
384 | |||
385 | } catch (ServerUnavailableException e) { | ||
386 | System.err.printf(SERVER_DOWN, base); | ||
387 | |||
388 | } catch (StatusException e) { | ||
389 | System.err.printf(SERVER_INTERNAL_ERROR, base, e.getStatus()); | ||
390 | |||
391 | } catch (IOException e) { | ||
392 | System.err.printf(SERVER_CANT_CONNECT, base, e); | ||
393 | } | ||
394 | |||
395 | REMAINING_PORTS.decrementAndGet(); | ||
396 | |||
397 | return null; | ||
398 | } | ||
399 | |||
400 | private static void reportStatus(int remaining) { | ||
401 | |||
402 | System.out.printf(STATUS, remaining, totalPorts, | ||
403 | Long.toString(Math.round((double) remaining / (double) totalPorts * 100)).replace(".0", "")); | ||
404 | } | ||
405 | |||
406 | private static String serializeServers(List<Server> servers) { | ||
407 | JSONObject baseJson = new JSONObject(); | ||
408 | JSONArray serversJson = new JSONArray(); | ||
409 | |||
410 | for (Server server : servers) { | ||
411 | JSONObject serverJson = new JSONObject(); | ||
412 | serverJson.put("host", server.getHost()); | ||
413 | serverJson.put("localization", server.getLocalization()); | ||
414 | |||
415 | serversJson.put(serverJson); | ||
416 | } | ||
417 | |||
418 | baseJson.put("servers", serversJson); | ||
419 | baseJson.put("created", System.currentTimeMillis()); | ||
420 | |||
421 | return baseJson.toString(); | ||
422 | } | ||
423 | |||
424 | } \ No newline at end of file | ||
diff --git a/src/main/resources/servers.json b/src/main/resources/servers.json deleted file mode 100644 index 7c73bd8..0000000 --- a/src/main/resources/servers.json +++ /dev/null | |||
@@ -1 +0,0 @@ | |||
1 | {"servers":[{"localization":"ARABIC","host":"srv2.akinator.com:9155"},{"localization":"KOREAN","host":"srv2.akinator.com:9156"},{"localization":"ENGLISH","host":"srv2.akinator.com:9157"},{"localization":"DUTCH","host":"srv2.akinator.com:9158"},{"localization":"ITALIAN","host":"srv2.akinator.com:9159"},{"localization":"SPANISH","host":"srv2.akinator.com:9160"},{"localization":"PORTUGUESE","host":"srv2.akinator.com:9161"},{"localization":"ENGLISH","host":"srv2.akinator.com:9162"},{"localization":"ENGLISH","host":"srv2.akinator.com:9163"},{"localization":"ENGLISH","host":"srv2.akinator.com:9255"},{"localization":"ITALIAN","host":"srv2.akinator.com:9262"},{"localization":"ENGLISH","host":"srv2.akinator.com:9265"},{"localization":"GERMAN","host":"srv2.akinator.com:9271"},{"localization":"KOREAN","host":"srv3.akinator.com:9205"},{"localization":"RUSSIAN","host":"srv3.akinator.com:9206"},{"localization":"ENGLISH","host":"srv3.akinator.com:9207"},{"localization":"PORTUGUESE","host":"srv3.akinator.com:9209"},{"localization":"ENGLISH","host":"srv3.akinator.com:9210"},{"localization":"TURKISH","host":"srv3.akinator.com:9211"},{"localization":"FRENCH","host":"srv3.akinator.com:9217"},{"localization":"FRENCH","host":"srv3.akinator.com:9218"},{"localization":"FRENCH","host":"srv3.akinator.com:9259"},{"localization":"JAPANESE","host":"srv3.akinator.com:9264"},{"localization":"HEBREW","host":"srv6.akinator.com:9179"},{"localization":"PORTUGUESE","host":"srv8.akinator.com:9292"},{"localization":"ENGLISH","host":"srv9.akinator.com:9200"},{"localization":"TURKISH","host":"srv9.akinator.com:9201"},{"localization":"CHINESE","host":"srv9.akinator.com:9202"},{"localization":"FRENCH","host":"srv9.akinator.com:9203"},{"localization":"JAPANESE","host":"srv9.akinator.com:9204"},{"localization":"ITALIAN","host":"srv9.akinator.com:9214"},{"localization":"DUTCH","host":"srv9.akinator.com:9215"},{"localization":"SPANISH","host":"srv9.akinator.com:9258"},{"localization":"ITALIAN","host":"srv9.akinator.com:9261"},{"localization":"ARABIC","host":"srv9.akinator.com:9273"},{"localization":"FRENCH","host":"srv9.akinator.com:9277"},{"localization":"CHINESE","host":"srv11.akinator.com:9150"},{"localization":"SPANISH","host":"srv11.akinator.com:9151"},{"localization":"ENGLISH","host":"srv11.akinator.com:9152"},{"localization":"JAPANESE","host":"srv11.akinator.com:9172"},{"localization":"PORTUGUESE","host":"srv11.akinator.com:9174"},{"localization":"GERMAN","host":"srv11.akinator.com:9254"},{"localization":"JAPANESE","host":"srv11.akinator.com:9263"},{"localization":"ENGLISH","host":"srv12.akinator.com:9184"},{"localization":"FRENCH","host":"srv12.akinator.com:9185"},{"localization":"JAPANESE","host":"srv12.akinator.com:9186"},{"localization":"ARABIC","host":"srv12.akinator.com:9187"},{"localization":"POLISH","host":"srv12.akinator.com:9188"},{"localization":"HEBREW","host":"srv12.akinator.com:9189"},{"localization":"RUSSIAN","host":"srv12.akinator.com:9190"},{"localization":"ENGLISH","host":"srv12.akinator.com:9193"},{"localization":"FRENCH","host":"srv12.akinator.com:9260"},{"localization":"FRENCH","host":"srv12.akinator.com:9276"},{"localization":"PORTUGUESE","host":"srv13.akinator.com:9195"},{"localization":"ENGLISH","host":"srv13.akinator.com:9196"},{"localization":"SPANISH","host":"srv13.akinator.com:9257"},{"localization":"ENGLISH","host":"srv13.akinator.com:9266"},{"localization":"RUSSIAN","host":"srv13.akinator.com:9272"},{"localization":"JAPANESE","host":"srv13.akinator.com:9280"},{"localization":"ENGLISH","host":"srv13.akinator.com:9287"},{"localization":"ENGLISH","host":"srv14.akinator.com:9281"},{"localization":"POLISH","host":"srv14.akinator.com:9282"},{"localization":"GERMAN","host":"srv14.akinator.com:9283"},{"localization":"GERMAN","host":"srv14.akinator.com:9284"},{"localization":"JAPANESE","host":"srv14.akinator.com:9285"},{"localization":"RUSSIAN","host":"srv14.akinator.com:9286"},{"localization":"FRENCH","host":"srv14.akinator.com:9288"},{"localization":"MALAY","host":"srv14.akinator.com:9290"},{"localization":"PORTUGUESE","host":"srv14.akinator.com:9291"},{"localization":"ENGLISH","host":"srv14.akinator.com:9293"}],"created":1564627232438} \ No newline at end of file | ||
diff --git a/src/test/java/com/markozajc/akiwrapper/IntegrationTest.java b/src/test/java/com/markozajc/akiwrapper/IntegrationTest.java new file mode 100644 index 0000000..94dc3a5 --- /dev/null +++ b/src/test/java/com/markozajc/akiwrapper/IntegrationTest.java | |||
@@ -0,0 +1,119 @@ | |||
1 | package com.markozajc.akiwrapper; | ||
2 | |||
3 | import java.util.List; | ||
4 | import java.util.stream.Stream; | ||
5 | |||
6 | import javax.annotation.Nonnull; | ||
7 | import javax.annotation.Nullable; | ||
8 | |||
9 | import org.junit.jupiter.params.ParameterizedTest; | ||
10 | import org.junit.jupiter.params.provider.Arguments; | ||
11 | import org.junit.jupiter.params.provider.MethodSource; | ||
12 | import org.slf4j.Logger; | ||
13 | import org.slf4j.LoggerFactory; | ||
14 | |||
15 | import com.markozajc.akiwrapper.Akiwrapper.Answer; | ||
16 | import com.markozajc.akiwrapper.core.entities.Guess; | ||
17 | import com.markozajc.akiwrapper.core.entities.Question; | ||
18 | import com.markozajc.akiwrapper.core.entities.Server.GuessType; | ||
19 | import com.markozajc.akiwrapper.core.entities.Server.Language; | ||
20 | import com.markozajc.akiwrapper.core.exceptions.ServerNotFoundException; | ||
21 | |||
22 | import static org.junit.jupiter.api.Assertions.assertEquals; | ||
23 | import static org.junit.jupiter.api.Assertions.assertFalse; | ||
24 | import static org.junit.jupiter.api.Assertions.assertNull; | ||
25 | import static org.junit.jupiter.api.Assertions.fail; | ||
26 | |||
27 | class IntegrationTest { | ||
28 | |||
29 | private static final String SERVER_GUESSTYPE_NO_MATCH = "The wanted and actual guess type of the server don't match."; | ||
30 | private static final String SERVER_LANGUAGE_NO_MATCH = "The wanted and actual language of the server don't match."; | ||
31 | private static final String QUESTION_CURRENT_NO_MATCH = "Current question does not match the one just returned by the API."; | ||
32 | private static final String QUESTION_WRONG_STEP = "Question was on an unexpected step."; | ||
33 | private static final String QUESTION_EMPTY = "Question mustn't be empty."; | ||
34 | private static final String QUESTION_NULL = "Question was null"; | ||
35 | private static final String QUESTION_INITIAL_NO_MATCH = "Initial question does not match the one after an equal amount of answers and undoes."; | ||
36 | |||
37 | @ParameterizedTest | ||
38 | @MethodSource("generateTestAkiwrapper") | ||
39 | void testAkiwrapper(@Nonnull Language language, @Nonnull GuessType guessType) { | ||
40 | Logger log = LoggerFactory.getLogger(String.format("%s-%s", language, guessType)); | ||
41 | log.info("Establishing connection"); | ||
42 | Akiwrapper api; | ||
43 | try { | ||
44 | api = new AkiwrapperBuilder().setLanguage(language).setGuessType(guessType).build(); | ||
45 | } catch (ServerNotFoundException e) { | ||
46 | log.warn("Current combination not supported, server wasn't found."); | ||
47 | log.trace("", e); | ||
48 | return; | ||
49 | } | ||
50 | |||
51 | log.info("Asserting the current state."); | ||
52 | Question initialQuestion = api.getCurrentQuestion(); | ||
53 | int expectedState = 0; | ||
54 | checkQuestion(initialQuestion, expectedState); | ||
55 | assertEquals(language, api.getServer().getLanguage(), SERVER_LANGUAGE_NO_MATCH); | ||
56 | assertEquals(guessType, api.getServer().getGuessType(), SERVER_GUESSTYPE_NO_MATCH); | ||
57 | log.trace("API server URL: {}", api.getServer().getUrl()); | ||
58 | |||
59 | log.info("Advancing {} steps (one time for each possible answer).", Answer.values().length); | ||
60 | for (Answer answer : Answer.values()) { | ||
61 | log.debug("Answering with {} and checking the question.", answer.name()); | ||
62 | Question newQuestion = api.answerCurrentQuestion(answer); | ||
63 | assertEquals(newQuestion, api.getCurrentQuestion(), QUESTION_CURRENT_NO_MATCH); | ||
64 | expectedState++; | ||
65 | checkQuestion(api.getCurrentQuestion(), expectedState); | ||
66 | } | ||
67 | |||
68 | log.info("Fetching guesses.", Answer.values().length); | ||
69 | List<Guess> guesses = api.getGuesses(); | ||
70 | debugGuesses(log, guesses); | ||
71 | |||
72 | log.info("Advancing -{} steps (using undo).", Answer.values().length); | ||
73 | for (int i = 0; i < Answer.values().length; i++) { | ||
74 | Question undoneQuestion = api.undoAnswer(); | ||
75 | assertEquals(undoneQuestion, api.getCurrentQuestion(), QUESTION_CURRENT_NO_MATCH); | ||
76 | expectedState--; | ||
77 | checkQuestion(api.getCurrentQuestion(), expectedState); | ||
78 | } | ||
79 | |||
80 | log.info("Asserting the final state."); | ||
81 | assertNull(api.undoAnswer()); | ||
82 | checkQuestion(api.getCurrentQuestion(), 0); | ||
83 | Question currentQuestion = api.getCurrentQuestion(); | ||
84 | if (initialQuestion != null && currentQuestion != null) { | ||
85 | assertEquals(initialQuestion.getQuestion(), currentQuestion.getQuestion(), QUESTION_INITIAL_NO_MATCH); | ||
86 | } | ||
87 | // Neither of those can be null due to checkQuestion checking (and failing on) | ||
88 | // nullability. | ||
89 | |||
90 | } | ||
91 | |||
92 | private static void debugGuesses(Logger log, List<Guess> guesses) { | ||
93 | log.debug("There are {} guesses.", guesses.size()); | ||
94 | for (Guess guess : guesses) { | ||
95 | log.trace("{} - {}", guess.getProbability(), guess.getName()); | ||
96 | } | ||
97 | } | ||
98 | |||
99 | private static void checkQuestion(@Nullable Question question, int expectedState) { | ||
100 | if (question == null) { | ||
101 | fail(QUESTION_NULL); | ||
102 | } else { | ||
103 | assertFalse(question.getQuestion().isEmpty(), QUESTION_EMPTY); | ||
104 | assertEquals(expectedState, question.getStep(), QUESTION_WRONG_STEP); | ||
105 | } | ||
106 | } | ||
107 | |||
108 | private static Stream<Arguments> generateTestAkiwrapper() { | ||
109 | Arguments[] arguments = new Arguments[Language.values().length * GuessType.values().length]; | ||
110 | int i = 0; | ||
111 | for (Language lang : Language.values()) | ||
112 | for (GuessType guessType : GuessType.values()) { | ||
113 | arguments[i] = Arguments.of(lang, guessType); | ||
114 | i++; | ||
115 | } | ||
116 | return Stream.of(arguments); | ||
117 | } | ||
118 | |||
119 | } | ||
diff --git a/src/test/java/com/markozajc/akiwrapper/core/RouteTest.java b/src/test/java/com/markozajc/akiwrapper/core/RouteTest.java new file mode 100644 index 0000000..1b3b7b2 --- /dev/null +++ b/src/test/java/com/markozajc/akiwrapper/core/RouteTest.java | |||
@@ -0,0 +1,40 @@ | |||
1 | package com.markozajc.akiwrapper.core; | ||
2 | |||
3 | import org.json.JSONObject; | ||
4 | import org.junit.jupiter.api.Test; | ||
5 | |||
6 | import com.markozajc.akiwrapper.core.exceptions.ServerUnavailableException; | ||
7 | import com.markozajc.akiwrapper.core.exceptions.StatusException; | ||
8 | |||
9 | import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; | ||
10 | import static org.junit.jupiter.api.Assertions.assertThrows; | ||
11 | |||
12 | class RouteTest { | ||
13 | |||
14 | @Test | ||
15 | void testMissingParameters() { | ||
16 | assertThrows(IllegalArgumentException.class, | ||
17 | () -> Route.NEW_SESSION.getRequest("", false /* and no parameters */)); | ||
18 | } | ||
19 | |||
20 | @Test | ||
21 | void testTestResponse() { | ||
22 | JSONObject base = new JSONObject(); | ||
23 | |||
24 | base.put("completion", "KO - SERVER DOWN"); | ||
25 | assertThrows(ServerUnavailableException.class, () -> Route.testResponse(base)); | ||
26 | |||
27 | base.put("completion", "KO - TEST REASON"); | ||
28 | assertThrows(StatusException.class, () -> Route.testResponse(base)); | ||
29 | |||
30 | base.put("completion", "WARN - TEST REASON"); | ||
31 | assertDoesNotThrow(() -> Route.testResponse(base)); | ||
32 | |||
33 | base.put("completion", "OK - TEST REASON"); | ||
34 | assertDoesNotThrow(() -> Route.testResponse(base)); | ||
35 | |||
36 | base.put("completion", "OK"); | ||
37 | assertDoesNotThrow(() -> Route.testResponse(base)); | ||
38 | } | ||
39 | |||
40 | } | ||
diff --git a/src/test/java/com/markozajc/akiwrapper/core/entities/IdentifiableTest.java b/src/test/java/com/markozajc/akiwrapper/core/entities/IdentifiableTest.java new file mode 100644 index 0000000..bbb987f --- /dev/null +++ b/src/test/java/com/markozajc/akiwrapper/core/entities/IdentifiableTest.java | |||
@@ -0,0 +1,35 @@ | |||
1 | package com.markozajc.akiwrapper.core.entities; | ||
2 | |||
3 | import org.junit.jupiter.api.Test; | ||
4 | |||
5 | import static org.junit.jupiter.api.Assertions.assertEquals; | ||
6 | import static org.junit.jupiter.api.Assertions.assertThrows; | ||
7 | import static org.junit.jupiter.api.Assertions.fail; | ||
8 | |||
9 | class IdentifiableTest { | ||
10 | |||
11 | @Test | ||
12 | void testGetIdLong() { | ||
13 | Identifiable identifiable = () -> "1234"; | ||
14 | assertEquals(1234, identifiable.getIdLong()); | ||
15 | } | ||
16 | |||
17 | @Test | ||
18 | void testGetIdLongLong() { | ||
19 | String maxLongString = Long.toString(Long.MAX_VALUE); | ||
20 | if (maxLongString == null) { | ||
21 | fail(); // Sorry suppress warnings broke and would let me ignore null warnings | ||
22 | return; // Also because eclipse doesn't realize that fail throws | ||
23 | } | ||
24 | |||
25 | Identifiable identifiable = () -> maxLongString; | ||
26 | assertEquals(Long.MAX_VALUE, identifiable.getIdLong()); | ||
27 | } | ||
28 | |||
29 | @Test | ||
30 | void testGetIdLongUnparsable() { | ||
31 | Identifiable identifiable = () -> "abcd"; | ||
32 | assertThrows(NumberFormatException.class, () -> identifiable.getIdLong()); | ||
33 | } | ||
34 | |||
35 | } | ||
diff --git a/src/test/java/com/markozajc/akiwrapper/core/entities/impl/immutable/ServerListImplTest.java b/src/test/java/com/markozajc/akiwrapper/core/entities/impl/immutable/ServerListImplTest.java new file mode 100644 index 0000000..4740187 --- /dev/null +++ b/src/test/java/com/markozajc/akiwrapper/core/entities/impl/immutable/ServerListImplTest.java | |||
@@ -0,0 +1,76 @@ | |||
1 | package com.markozajc.akiwrapper.core.entities.impl.immutable; | ||
2 | |||
3 | import static java.util.Arrays.asList; | ||
4 | |||
5 | import java.util.Arrays; | ||
6 | import java.util.Collections; | ||
7 | import java.util.List; | ||
8 | |||
9 | import org.junit.jupiter.api.Test; | ||
10 | |||
11 | import com.markozajc.akiwrapper.core.entities.Server; | ||
12 | import com.markozajc.akiwrapper.core.entities.Server.GuessType; | ||
13 | import com.markozajc.akiwrapper.core.entities.Server.Language; | ||
14 | import com.markozajc.akiwrapper.core.entities.ServerList; | ||
15 | |||
16 | import static org.junit.jupiter.api.Assertions.assertEquals; | ||
17 | import static org.junit.jupiter.api.Assertions.assertFalse; | ||
18 | import static org.junit.jupiter.api.Assertions.assertThrows; | ||
19 | import static org.junit.jupiter.api.Assertions.assertTrue; | ||
20 | |||
21 | class ServerListImplTest { | ||
22 | |||
23 | @SuppressWarnings("null") | ||
24 | @Test | ||
25 | void testEmptyCollection() { | ||
26 | List<Server> emptyList = Collections.emptyList(); | ||
27 | assertThrows(IllegalArgumentException.class, () -> new ServerListImpl(emptyList)); | ||
28 | } | ||
29 | |||
30 | @SuppressWarnings("null") | ||
31 | @Test | ||
32 | void testServersCollection() { | ||
33 | List<Server> serversList = asList(new ServerImpl("x", Language.ARABIC, GuessType.ANIMAL), | ||
34 | new ServerImpl("x", Language.FRENCH, GuessType.ANIMAL)); | ||
35 | ServerList serverList = new ServerListImpl(serversList); | ||
36 | assertEquals(serversList.size() - 1, serverList.getRemainingSize()); | ||
37 | assertEquals(serversList, serverList.getServers()); | ||
38 | assertEquals(Language.ARABIC, serverList.getLanguage()); | ||
39 | assertTrue(serverList.hasNext()); | ||
40 | assertTrue(serverList.next()); | ||
41 | assertEquals(Language.FRENCH, serverList.getLanguage()); | ||
42 | assertFalse(serverList.hasNext()); | ||
43 | assertFalse(serverList.next()); | ||
44 | } | ||
45 | |||
46 | @SuppressWarnings("null") | ||
47 | @Test | ||
48 | void testNestedServersCollection() { | ||
49 | List<Server> serversList = asList(new ServerImpl("x", Language.ARABIC, GuessType.ANIMAL), | ||
50 | new ServerImpl("x", Language.FRENCH, GuessType.ANIMAL)); | ||
51 | ServerList serverList = new ServerListImpl(Arrays.asList(new ServerListImpl(serversList))); | ||
52 | assertEquals(serversList.size() - 1, serverList.getRemainingSize()); | ||
53 | assertEquals(serversList, serverList.getServers()); | ||
54 | assertEquals(Language.ARABIC, serverList.getLanguage()); | ||
55 | assertTrue(serverList.hasNext()); | ||
56 | assertTrue(serverList.next()); | ||
57 | assertEquals(Language.FRENCH, serverList.getLanguage()); | ||
58 | assertFalse(serverList.hasNext()); | ||
59 | assertFalse(serverList.next()); | ||
60 | } | ||
61 | |||
62 | @Test | ||
63 | void testMixedServersCollection() { | ||
64 | ServerList serverList = | ||
65 | new ServerListImpl(new ServerImpl("x", Language.ARABIC, GuessType.ANIMAL), | ||
66 | new ServerListImpl(new ServerImpl("x", Language.FRENCH, GuessType.ANIMAL))); | ||
67 | assertEquals(2 /* amount of servers */ - 1, serverList.getRemainingSize()); | ||
68 | assertEquals(Language.ARABIC, serverList.getLanguage()); | ||
69 | assertTrue(serverList.hasNext()); | ||
70 | assertTrue(serverList.next()); | ||
71 | assertEquals(Language.FRENCH, serverList.getLanguage()); | ||
72 | assertFalse(serverList.hasNext()); | ||
73 | assertFalse(serverList.next()); | ||
74 | } | ||
75 | |||
76 | } | ||
diff --git a/src/test/java/com/markozajc/akiwrapper/core/entities/impl/immutable/StatusImplTest.java b/src/test/java/com/markozajc/akiwrapper/core/entities/impl/immutable/StatusImplTest.java new file mode 100644 index 0000000..95d4b56 --- /dev/null +++ b/src/test/java/com/markozajc/akiwrapper/core/entities/impl/immutable/StatusImplTest.java | |||
@@ -0,0 +1,53 @@ | |||
1 | package com.markozajc.akiwrapper.core.entities.impl.immutable; | ||
2 | |||
3 | import java.util.stream.Stream; | ||
4 | |||
5 | import javax.annotation.Nonnull; | ||
6 | |||
7 | import org.junit.jupiter.params.ParameterizedTest; | ||
8 | import org.junit.jupiter.params.provider.Arguments; | ||
9 | import org.junit.jupiter.params.provider.EnumSource; | ||
10 | import org.junit.jupiter.params.provider.EnumSource.Mode; | ||
11 | import org.junit.jupiter.params.provider.MethodSource; | ||
12 | |||
13 | import com.markozajc.akiwrapper.core.entities.Status; | ||
14 | import com.markozajc.akiwrapper.core.entities.Status.Level; | ||
15 | |||
16 | import static org.junit.jupiter.api.Assertions.assertEquals; | ||
17 | import static org.junit.jupiter.api.Assertions.assertNull; | ||
18 | |||
19 | class StatusImplTest { | ||
20 | |||
21 | @ParameterizedTest | ||
22 | @EnumSource( | ||
23 | value = Level.class, | ||
24 | mode = Mode.EXCLUDE) | ||
25 | void testStringConstructorNoReason(@Nonnull Level level) { | ||
26 | @SuppressWarnings("null") | ||
27 | Status status = new StatusImpl(level.toString()); | ||
28 | assertEquals(level, status.getLevel()); | ||
29 | assertNull(status.getReason()); | ||
30 | } | ||
31 | |||
32 | @ParameterizedTest | ||
33 | @MethodSource("generateTestStringConstructorWithReason") | ||
34 | void testStringConstructorWithReason(@Nonnull Level level, @Nonnull String reason) { | ||
35 | String completion = level.toString() + " - " + reason; | ||
36 | Status status = new StatusImpl(completion); | ||
37 | assertEquals(level, status.getLevel()); | ||
38 | assertEquals(reason, status.getReason()); | ||
39 | } | ||
40 | |||
41 | private static Stream<Arguments> generateTestStringConstructorWithReason() { | ||
42 | String[] reasons = { "", "reason", "reason with spaces", "UPPERCASE", "UPPERCASE WITH SPACES" }; | ||
43 | Arguments[] arguments = new Arguments[Level.values().length * reasons.length]; | ||
44 | int i = 0; | ||
45 | for (Level level : Level.values()) | ||
46 | for (String reason : reasons) { | ||
47 | arguments[i] = Arguments.of(level, reason); | ||
48 | i++; | ||
49 | } | ||
50 | return Stream.of(arguments); | ||
51 | } | ||
52 | |||
53 | } | ||
diff --git a/src/test/java/com/markozajc/akiwrapper/core/utils/JSONUtilsTest.java b/src/test/java/com/markozajc/akiwrapper/core/utils/JSONUtilsTest.java new file mode 100644 index 0000000..4780f0c --- /dev/null +++ b/src/test/java/com/markozajc/akiwrapper/core/utils/JSONUtilsTest.java | |||
@@ -0,0 +1,68 @@ | |||
1 | package com.markozajc.akiwrapper.core.utils; | ||
2 | |||
3 | import org.json.JSONObject; | ||
4 | import org.junit.jupiter.api.Test; | ||
5 | |||
6 | import static org.junit.jupiter.api.Assertions.assertEquals; | ||
7 | import static org.junit.jupiter.api.Assertions.assertNull; | ||
8 | import static org.junit.jupiter.api.Assertions.assertThrows; | ||
9 | |||
10 | class JSONUtilsTest { | ||
11 | |||
12 | // @formatter:off | ||
13 | public static final String TEST_GET_INT_JSON = | ||
14 | "{ "+ | ||
15 | " \"test1\": \"1\", "+ | ||
16 | " \"test2\": 2, "+ | ||
17 | " \"test3\": \"not an integer\", "+ | ||
18 | " \"test4\": {} "+ | ||
19 | "} "; | ||
20 | public static final String TEST_GET_STRING_JSON = | ||
21 | "{ "+ | ||
22 | " \"test1\": \"string\", "+ | ||
23 | " \"test2\": 's', "+ | ||
24 | " \"test3\": 1 "+ | ||
25 | "} "; | ||
26 | public static final String TEST_GET_DOUBLE_JSON = | ||
27 | "{ "+ | ||
28 | " \"test1\": \"1\", "+ | ||
29 | " \"test2\": \"0.5\", "+ | ||
30 | " \"test3\": 2, "+ | ||
31 | " \"test4\": 1.5, "+ | ||
32 | " \"test5\": \"not a double\", "+ | ||
33 | " \"test6\": {} "+ | ||
34 | "} "; | ||
35 | // @formatter:on | ||
36 | |||
37 | @Test | ||
38 | void testGetInt() { | ||
39 | JSONObject json = new JSONObject(TEST_GET_INT_JSON); | ||
40 | assertEquals(1, JSONUtils.getInteger(json, "test1").orElse(null)); | ||
41 | assertEquals(2, JSONUtils.getInteger(json, "test2").orElse(null)); | ||
42 | assertThrows(NumberFormatException.class, () -> JSONUtils.getInteger(json, "test3")); | ||
43 | assertThrows(NumberFormatException.class, () -> JSONUtils.getInteger(json, "test4")); | ||
44 | assertNull(JSONUtils.getInteger(json, "test5").orElse(null)); | ||
45 | } | ||
46 | |||
47 | @Test | ||
48 | void testGetString() { | ||
49 | JSONObject json = new JSONObject(TEST_GET_STRING_JSON); | ||
50 | assertEquals("string", JSONUtils.getString(json, "test1").orElse(null)); | ||
51 | assertEquals("s", JSONUtils.getString(json, "test2").orElse(null)); | ||
52 | assertEquals("1", JSONUtils.getString(json, "test3").orElse(null)); | ||
53 | assertNull(JSONUtils.getInteger(json, "test4").orElse(null)); | ||
54 | } | ||
55 | |||
56 | @Test | ||
57 | void testGetDouble() { | ||
58 | JSONObject json = new JSONObject(TEST_GET_DOUBLE_JSON); | ||
59 | assertEquals(1d, JSONUtils.getDouble(json, "test1").orElse(null)); | ||
60 | assertEquals(0.5d, JSONUtils.getDouble(json, "test2").orElse(null)); | ||
61 | assertEquals(2d, JSONUtils.getDouble(json, "test3").orElse(null)); | ||
62 | assertEquals(1.5d, JSONUtils.getDouble(json, "test4").orElse(null)); | ||
63 | assertThrows(NumberFormatException.class, () -> JSONUtils.getInteger(json, "test5")); | ||
64 | assertThrows(NumberFormatException.class, () -> JSONUtils.getInteger(json, "test6")); | ||
65 | assertNull(JSONUtils.getInteger(json, "test7").orElse(null)); | ||
66 | } | ||
67 | |||
68 | } | ||
diff --git a/src/test/java/com/markozajc/akiwrapper/tests/TestServerAvailability.java b/src/test/java/com/markozajc/akiwrapper/tests/TestServerAvailability.java deleted file mode 100644 index a0fa3fa..0000000 --- a/src/test/java/com/markozajc/akiwrapper/tests/TestServerAvailability.java +++ /dev/null | |||
@@ -1,64 +0,0 @@ | |||
1 | package com.markozajc.akiwrapper.tests; | ||
2 | |||
3 | import org.junit.jupiter.api.Assertions; | ||
4 | import org.junit.jupiter.api.Test; | ||
5 | |||
6 | import com.markozajc.akiwrapper.core.entities.Server; | ||
7 | import com.markozajc.akiwrapper.core.entities.Server.Language; | ||
8 | import com.markozajc.akiwrapper.core.entities.ServerGroup; | ||
9 | import com.markozajc.akiwrapper.core.utils.Servers; | ||
10 | |||
11 | class TestServerAvailability { | ||
12 | |||
13 | /** | ||
14 | * Whether to fail if a {@link Server} is unavailable. | ||
15 | */ | ||
16 | private static final boolean FAIL_IF_SERVER_DOWN = false; | ||
17 | /** | ||
18 | * Whether to fail if a {@link ServerGroup} (a {@link Language}) is unavailable. | ||
19 | */ | ||
20 | private static final boolean FAIL_IF_GROUP_DOWN = false; | ||
21 | |||
22 | private static final String GROUP_DOWN = "[FATAL] %s is down!\n"; | ||
23 | private static final String GROUP_AVAILABLE = "[INFO] %s is up with %s available servers.\n"; | ||
24 | private static final String SERVER_DOWN = "\t[WARNING] %s-%s is down!\n"; | ||
25 | private static final String TESTING_SERVER = "\t[DEBUG] Testing %s: %s/%s\n"; | ||
26 | private static final String TESTING_GROUP = "[INFO] Testing %s\n"; | ||
27 | |||
28 | @Test | ||
29 | void testServers() { | ||
30 | Servers.SERVER_GROUPS.values().forEach(g -> { | ||
31 | |||
32 | System.out.printf(TESTING_GROUP, g.getLocalization().toString()); | ||
33 | |||
34 | int available = 0; | ||
35 | int size = g.getServers().size(); | ||
36 | for (int i = 0; i < size; i++) { | ||
37 | System.out.printf(TESTING_SERVER, g.getLocalization().toString(), i + 1, size); | ||
38 | |||
39 | if (g.getServers().get(i).isUp()) { | ||
40 | available++; | ||
41 | |||
42 | } else { | ||
43 | System.err.printf(SERVER_DOWN, g.getLocalization(), i + 1); | ||
44 | |||
45 | if (FAIL_IF_SERVER_DOWN) | ||
46 | Assertions.fail(); | ||
47 | } | ||
48 | |||
49 | } | ||
50 | |||
51 | if (available > 0) { | ||
52 | System.out.printf(GROUP_AVAILABLE, g.getLocalization().toString(), available); | ||
53 | |||
54 | } else { | ||
55 | System.err.printf(GROUP_DOWN, g.getLocalization().toString()); | ||
56 | |||
57 | if (FAIL_IF_GROUP_DOWN) | ||
58 | Assertions.fail(); | ||
59 | } | ||
60 | |||
61 | }); | ||
62 | } | ||
63 | |||
64 | } | ||
diff --git a/src/test/resources/simplelogger.properties b/src/test/resources/simplelogger.properties new file mode 100644 index 0000000..cc84500 --- /dev/null +++ b/src/test/resources/simplelogger.properties | |||
@@ -0,0 +1,7 @@ | |||
1 | org.slf4j.simpleLogger.showDateTime = false | ||
2 | org.slf4j.simpleLogger.showLogName = false | ||
3 | org.slf4j.simpleLogger.showShortLogName = true | ||
4 | org.slf4j.simpleLogger.showThreadName = false | ||
5 | org.slf4j.simpleLogger.logFile = System.out | ||
6 | org.slf4j.simpleLogger.levelInBrackets = true | ||
7 | org.slf4j.simpleLogger.defaultLogLevel = info \ No newline at end of file | ||