diff options
42 files changed, 1713 insertions, 1304 deletions
@@ -9,13 +9,13 @@ Add the following dependency to your pom.xml: | |||
9 | <dependency> | 9 | <dependency> |
10 | <groupId>com.github.markozajc</groupId> | 10 | <groupId>com.github.markozajc</groupId> |
11 | <artifactId>akiwrapper</artifactId> | 11 | <artifactId>akiwrapper</artifactId> |
12 | <version>1.5.2</version> | 12 | <version>1.6</version> |
13 | </dependency> | 13 | </dependency> |
14 | ``` | 14 | ``` |
15 | #### Gradle | 15 | #### Gradle |
16 | Add the following dependency to your build.gradle: | 16 | Add the following dependency to your build.gradle: |
17 | ```gradle | 17 | ```gradle |
18 | implementation group: 'com.github.markozajc', name: 'akiwrapper', version: '1.5.2' | 18 | implementation group: 'com.github.markozajc', name: 'akiwrapper', version: '1.6' |
19 | ``` | 19 | ``` |
20 | 20 | ||
21 | ## Usage | 21 | ## Usage |
@@ -28,44 +28,52 @@ If you, for example, wish to use a different language that the default English, | |||
28 | something other than characters, you may use the following setup: | 28 | something other than characters, you may use the following setup: |
29 | ```java | 29 | ```java |
30 | Akiwrapper aw = new AkiwrapperBuilder() | 30 | Akiwrapper aw = new AkiwrapperBuilder() |
31 | .setLanguage(Language.GERMAN) | 31 | .setLanguage(Language.GERMAN) |
32 | .setGuessType(GuessType.PLACE) | 32 | .setGuessType(GuessType.PLACE) |
33 | .build(); | 33 | .build(); |
34 | ``` | 34 | ``` |
35 | (keep in mind that not all language-guesstype combinations are supported, though all languages support `CHARACTER`) | 35 | (keep in mind that not all language-guesstype combinations are supported, though all languages support `CHARACTER`) |
36 | 36 | ||
37 | You'll likely want to set up a question-answer loop afterwards. Fetch questions with | 37 | You'll typically want to set up a question-answer loop afterwards. Fetch questions with |
38 | ```java | 38 | ```java |
39 | Question question = aw.getQuestion(); | 39 | Question question = aw.getQuestion(); |
40 | ``` | 40 | ``` |
41 | 41 | ||
42 | Display the question to the user, collect their answer, and feed it to Akinator with | 42 | Display the question to the player, collect their answer, and feed it to Akinator with |
43 | ```java | 43 | ```java |
44 | aw.answer(Answer.YES); | 44 | aw.answer(Answer.YES); |
45 | ``` | 45 | ``` |
46 | 46 | ||
47 | If the player wishes to undo their previous answer, you can let Akinator know with | 47 | If the player wishes to undo their previous answer, you can let do that with |
48 | ```java | 48 | ```java |
49 | aw.undoAnswer(); | 49 | aw.undoAnswer(); |
50 | ``` | 50 | ``` |
51 | You can undo answers all the way to the first question. | ||
51 | 52 | ||
52 | Akinator will propose a list of guesses after each answer, coupled with their determined probabilities. You can get all | 53 | Akinator will occasionally try guessing what the player is thinking about. |
53 | guesses above a certain probability with | ||
54 | ```java | 54 | ```java |
55 | aw.getGuessesAboveProbability(0.85f); // 85% seems to be the sweet spot, though you're free to use anything you want | 55 | var guess = aw.suggestGuess() |
56 | if (guess != null) { | ||
57 | // ask the player to confirm or reject the guess | ||
58 | if (playerConfirmedGuess) { | ||
59 | aw.confirmGuess(guess); // let Akinator know that the guess is right | ||
60 | return; // finish the game | ||
61 | |||
62 | } else { | ||
63 | aw.rejectLastGuess(); // let Akinator know that the guess is not right - this also gives us a new question | ||
64 | } | ||
65 | } | ||
66 | ; | ||
56 | ``` | 67 | ``` |
57 | Let the player review each guess, but keep track of the declined ones, as Akinator will send you the same guesses over | 68 | When a guess is available, the player should be asked to confirm it. If the guess is confirmed, we finish the game and |
58 | and over if he feels like it. | 69 | optionally let Akinator know. If the guess is rejected, we let Akinator know and continue. Akiwrapper also keeps track |
70 | of rejected guesses for you, so `suggestGuess()` never returns the same guess. | ||
59 | 71 | ||
60 | At some point Akinator will run out of questions to ask. This is indicated by `aw.getCurrentQuestion()` equalling null. | 72 | At some point (normally after question #80) Akinator will run out of questions to ask. This is indicated by |
61 | If and when this happens, fetch and propose all remaining guesses (this time without a probability filter) with | 73 | `aw.isExhausted()`. After there are no questions left, the last guess should be retrieved and shown to the player. |
62 | ```java | ||
63 | aw.getGuesses() | ||
64 | ``` | ||
65 | and propose each one to the player. This also marks the absolute end of the game. | ||
66 | 74 | ||
67 | Unless you provide your own UnirestInstance to AkiwrapperBuilder, you should make sure to shut down the singleton | 75 | Unless you provide your own UnirestInstance to AkiwrapperBuilder, you should make sure to shut down the singleton |
68 | instance that Akiwrapper uses by default after you're done with Akiwrapper: | 76 | instance that Akiwrapper uses by default after you're done with Akiwrapper (calling `System.exit()` also works): |
69 | ```java | 77 | ```java |
70 | UnirestUtils.shutdownInstance(); | 78 | UnirestUtils.shutdownInstance(); |
71 | ``` | 79 | ``` |
diff --git a/example/src/main/java/zajc/akiwrapper/AkinatorExample.java b/example/src/main/java/zajc/akiwrapper/AkinatorExample.java index 148394f..0a903e6 100644 --- a/example/src/main/java/zajc/akiwrapper/AkinatorExample.java +++ b/example/src/main/java/zajc/akiwrapper/AkinatorExample.java | |||
@@ -3,7 +3,7 @@ package zajc.akiwrapper; | |||
3 | import static com.github.markozajc.akiwrapper.Akiwrapper.Answer.*; | 3 | import static com.github.markozajc.akiwrapper.Akiwrapper.Answer.*; |
4 | import static com.github.markozajc.akiwrapper.core.entities.Server.GuessType.CHARACTER; | 4 | import static com.github.markozajc.akiwrapper.core.entities.Server.GuessType.CHARACTER; |
5 | import static com.github.markozajc.akiwrapper.core.entities.Server.Language.ENGLISH; | 5 | import static com.github.markozajc.akiwrapper.core.entities.Server.Language.ENGLISH; |
6 | import static java.lang.Integer.parseInt; | 6 | import static java.lang.Character.toLowerCase; |
7 | import static java.lang.System.*; | 7 | import static java.lang.System.*; |
8 | import static java.util.stream.Collectors.joining; | 8 | import static java.util.stream.Collectors.joining; |
9 | 9 | ||
@@ -19,121 +19,82 @@ import com.github.markozajc.akiwrapper.core.exceptions.ServerNotFoundException; | |||
19 | @SuppressWarnings("javadoc") | 19 | @SuppressWarnings("javadoc") |
20 | public class AkinatorExample { | 20 | public class AkinatorExample { |
21 | 21 | ||
22 | public static final double PROBABILITY_THRESHOLD = 0.85; | 22 | private static final String ANSWER_TIP = |
23 | // This is used to determine which guesses are probable (probability is determined by | 23 | "Y or yes, N or no, DK or don't know, P or probably, PN or probably not, or go back one step with B or back."; |
24 | // Akinator) enough to propose to the player. | 24 | private static final Scanner IN = new Scanner(System.in).useDelimiter("\n"); |
25 | 25 | ||
26 | @SuppressWarnings("null") | 26 | public static void main(String[] args) { |
27 | public static void main(String[] args) throws Exception { | 27 | boolean filterProfanity = getProfanityFilter(); |
28 | try (var in = new Scanner(System.in)) { | 28 | // Gets player's age. Like the Akinator's website, this will turn on the profanity |
29 | boolean filterProfanity = getProfanityFilter(in); | 29 | // filter if the age entered is below 16. |
30 | // Gets player's age. Like the Akinator's website, this will turn on the profanity | 30 | |
31 | // filter if the age entered is below 16. | 31 | var language = getLanguage(); |
32 | 32 | // Gets player's language. Akinator will give the user localized questions and | |
33 | var language = getLanguage(in); | 33 | // guesses depending on user's language. |
34 | // Gets player's language. Akinator will give the user localized questions and | 34 | |
35 | // guesses depending on user's language. | 35 | var guessType = getGuessType(); |
36 | 36 | // Gets the guess type. | |
37 | var guessType = getGuessType(in); | 37 | |
38 | // Gets the guess type. | 38 | Akiwrapper aw; |
39 | 39 | try { | |
40 | Akiwrapper aw; | 40 | aw = new AkiwrapperBuilder().setFilterProfanity(filterProfanity) |
41 | try { | 41 | .setLanguage(language) |
42 | aw = new AkiwrapperBuilder().setFilterProfanity(filterProfanity) | 42 | .setGuessType(guessType) |
43 | .setLanguage(language) | 43 | .build(); |
44 | .setGuessType(guessType) | 44 | } catch (ServerNotFoundException e) { |
45 | .build(); | 45 | err.println("Unsupported combination of language and guess type"); |
46 | } catch (ServerNotFoundException e) { | 46 | return; |
47 | err.println("Unsupported combination of language and guess type"); | ||
48 | return; | ||
49 | } | ||
50 | // Builds the Akiwrapper instance, this is what we'll be using to perform | ||
51 | // operations such as answering questions, fetching guesses, etc | ||
52 | |||
53 | var rejected = new ArrayList<Long>(); | ||
54 | // A list of rejected guesses, used to prevent them from repeating | ||
55 | |||
56 | while (aw.getQuestion() != null) { | ||
57 | // Runs while there are still questions left | ||
58 | |||
59 | Question question = aw.getQuestion(); | ||
60 | if (question == null) | ||
61 | break; | ||
62 | // Breaks the loop if question is null; /should/ not occur, but safety is still | ||
63 | // first | ||
64 | |||
65 | out.printf("Question #%d%n", question.getStep() + 1); | ||
66 | out.printf("\t%s%n", question.getQuestion()); | ||
67 | // Displays the question. | ||
68 | |||
69 | if (question.getStep() == 0) | ||
70 | out.printf("%nAnswer with " + | ||
71 | "Y (yes), N (no), DK (don't know), P (probably) or PN (probably not) " + | ||
72 | "or go back in time with B (back).%n"); | ||
73 | // Displays the tip (only for the first time) | ||
74 | |||
75 | answerQuestion(in, aw); | ||
76 | |||
77 | reviewGuesses(in, aw, rejected); | ||
78 | // Iterates over any available guesses. | ||
79 | } | ||
80 | |||
81 | for (Guess guess : aw.getGuesses()) { | ||
82 | if (reviewGuess(guess, in)) { | ||
83 | // Reviews all final guesses. | ||
84 | finish(true); | ||
85 | exit(0); | ||
86 | } | ||
87 | } | ||
88 | |||
89 | finish(false); | ||
90 | // Loses if all guesses are rejected. | ||
91 | } | 47 | } |
92 | } | 48 | // Builds the Akiwrapper instance, this is what we'll be using to perform |
49 | // operations such as answering questions, fetching guesses, etc | ||
93 | 50 | ||
94 | private static void answerQuestion(@Nonnull Scanner sc, @Nonnull Akiwrapper aw) { | 51 | while (aw.getQuestion() != null) { |
95 | boolean answered = false; | 52 | // Runs while there are still questions left |
96 | while (!answered) { | ||
97 | // Iterates while the questions remains unanswered. | ||
98 | 53 | ||
99 | var answer = sc.nextLine().toLowerCase(); | 54 | Question question = aw.getQuestion(); |
55 | if (question == null) | ||
56 | break; | ||
57 | // Breaks the loop if question is null; /should/ not occur, but safety is still | ||
58 | // first | ||
100 | 59 | ||
101 | if (answer.equals("y")) { | 60 | out.printf("Question #%d%n\t%s%n", question.getStep() + 1, question.getQuestion()); |
102 | aw.answer(YES); | 61 | // Displays the question. |
103 | 62 | ||
104 | } else if (answer.equals("n")) { | 63 | if (question.getStep() == 0) |
105 | aw.answer(NO); | 64 | out.printf("%nAnswer with %s%n", ANSWER_TIP); |
65 | // Displays the tip (only for the first time) | ||
106 | 66 | ||
107 | } else if (answer.equals("dk")) { | 67 | out.print("> "); |
108 | aw.answer(DONT_KNOW); | ||
109 | 68 | ||
110 | } else if (answer.equals("p")) { | 69 | answerQuestion(aw); |
111 | aw.answer(PROBABLY); | 70 | // Displays the question and prompts the player for an answer |
112 | 71 | ||
113 | } else if (answer.equals("pn")) { | 72 | reviewSuggestedGuess(aw); |
114 | aw.answer(PROBABLY_NOT); | 73 | // Checks if any guess is available and prompts the player |
74 | } | ||
115 | 75 | ||
116 | } else if (answer.equals("b")) { | 76 | reviewSuggestedGuess(aw); |
117 | aw.undoAnswer(); | 77 | // Reviews the final guess |
118 | 78 | ||
119 | } else if (answer.equals("debug")) { | 79 | finish(false); |
120 | out.printf("Debug information:%n\tCurrent API server: %s%n\tCurrent guess count: %d%n", | 80 | // Loses if all guesses are rejected. |
121 | aw.getServer().getUrl(), aw.getGuesses().size()); | 81 | } |
122 | continue; | ||
123 | // Displays some debug information. | ||
124 | 82 | ||
83 | private static Guess reviewSuggestedGuess(@Nonnull Akiwrapper aw) { | ||
84 | var guess = aw.suggestGuess(); | ||
85 | if (guess != null) { | ||
86 | if (reviewGuess(guess)) { | ||
87 | aw.confirmGuess(guess); | ||
88 | finish(true); | ||
89 | exit(0); | ||
125 | } else { | 90 | } else { |
126 | out.println("Please answer with either " + | 91 | aw.rejectLastGuess(); |
127 | "[Y]ES, [N]O, [D|ONT |K]NOW, [P]ROBABLY or [P|ROBABLY |N]OT or go back one step with [B]ACK."); | ||
128 | continue; | ||
129 | } | 92 | } |
130 | |||
131 | answered = true; | ||
132 | // Answers the question. | ||
133 | } | 93 | } |
94 | return guess; | ||
134 | } | 95 | } |
135 | 96 | ||
136 | private static boolean reviewGuess(@Nonnull Guess guess, @Nonnull Scanner sc) { | 97 | private static boolean reviewGuess(@Nonnull Guess guess) { |
137 | out.println(guess.getName()); | 98 | out.println(guess.getName()); |
138 | out.print("\t"); | 99 | out.print("\t"); |
139 | if (guess.getDescription() == null) | 100 | if (guess.getDescription() == null) |
@@ -143,49 +104,77 @@ public class AkinatorExample { | |||
143 | out.println(); | 104 | out.println(); |
144 | // Displays the guess' information | 105 | // Displays the guess' information |
145 | 106 | ||
146 | boolean answered = false; | 107 | out.print("Is this what you're thinking of? [Y/n] "); |
147 | boolean isCharacter = false; | 108 | var input = IN.next().trim(); |
148 | while (!answered) { | 109 | // Asks the player if the guess is correct |
149 | // Asks the player if the guess is correct | 110 | |
111 | return input.isEmpty() || toLowerCase(input.charAt(0)) == 'y'; | ||
112 | } | ||
113 | |||
114 | private static void answerQuestion(@Nonnull Akiwrapper aw) { | ||
115 | main: while (true) { | ||
116 | // Prompts the player for an answer | ||
117 | |||
118 | var input = IN.next().toLowerCase(); | ||
150 | 119 | ||
151 | out.println("Is this your character? (y/n)"); | 120 | switch (input) { |
152 | String line = sc.nextLine(); | ||
153 | switch (line) { | ||
154 | case "y": | 121 | case "y": |
155 | // If the player has responded positively. | 122 | case "yes": |
156 | answered = true; | 123 | aw.answer(YES); |
157 | isCharacter = true; | 124 | break main; |
158 | break; | ||
159 | 125 | ||
160 | case "n": | 126 | case "n": |
161 | // If the player has responded negatively. | 127 | case "no": |
162 | answered = true; | 128 | aw.answer(NO); |
163 | isCharacter = false; | 129 | break main; |
130 | |||
131 | case "dk": | ||
132 | case "don'tknow": | ||
133 | case "dontknow": | ||
134 | case "dont know": | ||
135 | case "don't know": | ||
136 | aw.answer(DONT_KNOW); | ||
137 | break main; | ||
138 | |||
139 | case "p": | ||
140 | case "probably": | ||
141 | aw.answer(PROBABLY); | ||
142 | break main; | ||
143 | |||
144 | case "pn": | ||
145 | case "probablynot": | ||
146 | case "probably not": | ||
147 | aw.answer(PROBABLY_NOT); | ||
148 | break main; | ||
149 | |||
150 | case "b": | ||
151 | case "back": | ||
152 | if (aw.getStep() == 0) | ||
153 | out.println("Can't undo on the first question."); | ||
154 | else | ||
155 | aw.undoAnswer(); | ||
156 | break main; | ||
157 | |||
158 | case "debug": | ||
159 | displayDebug(aw); | ||
164 | break; | 160 | break; |
161 | // Displays some debug information. | ||
165 | 162 | ||
166 | default: | 163 | default: |
164 | out.println("Please answer with either " + ANSWER_TIP); | ||
167 | break; | 165 | break; |
168 | } | 166 | } |
169 | } | 167 | } |
170 | |||
171 | return isCharacter; | ||
172 | } | 168 | } |
173 | 169 | ||
174 | private static void reviewGuesses(@Nonnull Scanner sc, @Nonnull Akiwrapper aw, @Nonnull List<Long> declined) { | 170 | private static void displayDebug(Akiwrapper aw) { |
175 | for (var guess : aw.getGuessesAboveProbability(PROBABILITY_THRESHOLD)) { | 171 | var question = aw.getQuestion(); |
176 | if (!declined.contains(Long.valueOf(guess.getIdLong()))) { | 172 | out.println("Debug information:"); |
177 | // Checks if this guess complies with the conditions. | 173 | out.printf("\tCurrent API server: %s%n", aw.getServer().getUrl()); |
178 | 174 | out.printf("\tProgression: %f%%%n", question == null ? -1 : question.getProgression()); | |
179 | if (reviewGuess(guess, sc)) { | 175 | out.println("\tGuesses:"); |
180 | // If the player accepts this guess | 176 | aw.getGuesses().stream().forEach(g -> out.printf("\t\t%f - %s%n", g.getProbability(), g.getName())); |
181 | finish(true); | 177 | out.print("> "); |
182 | exit(0); | ||
183 | } | ||
184 | |||
185 | declined.add(guess.getIdLong()); | ||
186 | // Registers this guess as rejected. | ||
187 | } | ||
188 | } | ||
189 | } | 178 | } |
190 | 179 | ||
191 | private static void finish(boolean win) { | 180 | private static void finish(boolean win) { |
@@ -200,67 +189,53 @@ public class AkinatorExample { | |||
200 | } | 189 | } |
201 | } | 190 | } |
202 | 191 | ||
203 | private static boolean getProfanityFilter(@Nonnull Scanner sc) { | 192 | private static boolean getProfanityFilter() { |
204 | out.println("What's your age? (default: 18)"); | 193 | out.print("Enable profanity filtering? [y/N] "); |
205 | while (true) { | 194 | var input = IN.next().trim(); |
206 | var age = sc.nextLine(); | 195 | return !input.isEmpty() && toLowerCase(input.charAt(0)) == 'y'; |
207 | |||
208 | if (age.equals("")) | ||
209 | return false; | ||
210 | |||
211 | try { | ||
212 | return parseInt(age) < 16; | ||
213 | } catch (NumberFormatException e) { | ||
214 | out.println("That's not a number"); | ||
215 | } | ||
216 | } | ||
217 | } | 196 | } |
218 | 197 | ||
219 | @Nonnull | 198 | @Nonnull |
220 | private static Language getLanguage(@Nonnull Scanner sc) { | 199 | @SuppressWarnings("null") |
200 | private static Language getLanguage() { | ||
221 | var languages = EnumSet.allOf(Language.class); | 201 | var languages = EnumSet.allOf(Language.class); |
222 | 202 | ||
223 | out.println("What's your language? (default: English)"); | 203 | out.print("What's your language? [English] "); |
224 | while (true) { | 204 | while (true) { |
225 | String selectedLanguage = sc.nextLine().toLowerCase().trim(); | 205 | var input = IN.next().trim().toUpperCase(); |
226 | 206 | ||
227 | if (selectedLanguage.equals("")) | 207 | if (input.isEmpty()) |
228 | return ENGLISH; | 208 | return ENGLISH; |
229 | 209 | ||
230 | var language = languages.stream() | 210 | var language = languages.stream().filter(l -> l.toString().equals(input)).findAny(); |
231 | .filter(l -> l.toString().toLowerCase().equals(selectedLanguage)) | ||
232 | .findAny() | ||
233 | .orElse(null); | ||
234 | 211 | ||
235 | if (language != null) { | 212 | if (language.isPresent()) { |
236 | return language; | 213 | return language.orElseThrow(); |
237 | 214 | ||
238 | } else { | 215 | } else { |
239 | out.println(languages.stream() | 216 | out.println(languages.stream() |
240 | .map(Enum::toString) | 217 | .map(Enum::toString) |
241 | .collect(joining("\n-", "Sorry, that language isn't supported. Choose between\n-", ""))); | 218 | .collect(joining("\n-", "Sorry, that language isn't supported. Available options:\n-", ""))); |
242 | } | 219 | } |
243 | } | 220 | } |
244 | } | 221 | } |
245 | 222 | ||
246 | @Nonnull | 223 | @Nonnull |
247 | private static GuessType getGuessType(@Nonnull Scanner sc) { | 224 | @SuppressWarnings("null") |
225 | private static GuessType getGuessType() { | ||
248 | var guessTypes = EnumSet.allOf(GuessType.class); | 226 | var guessTypes = EnumSet.allOf(GuessType.class); |
249 | 227 | ||
250 | out.println("What will you be guessing? (default: character)"); | 228 | out.print("What will you be guessing? [character] "); |
251 | while (true) { | 229 | while (true) { |
252 | String selectedGuessType = sc.nextLine().toLowerCase().trim(); | 230 | var input = IN.next().trim().toUpperCase(); |
253 | 231 | ||
254 | if (selectedGuessType.equals("")) | 232 | if (input.isEmpty()) |
255 | return CHARACTER; | 233 | return CHARACTER; |
256 | 234 | ||
257 | var guessType = guessTypes.stream() | 235 | var guessType = guessTypes.stream().filter(l -> l.toString().equals(input)).findAny(); |
258 | .filter(l -> l.toString().toLowerCase().equals(selectedGuessType)) | ||
259 | .findAny() | ||
260 | .orElse(null); | ||
261 | 236 | ||
262 | if (guessType != null) { | 237 | if (guessType.isPresent()) { |
263 | return guessType; | 238 | return guessType.orElseThrow(); |
264 | 239 | ||
265 | } else { | 240 | } else { |
266 | out.println("" + guessTypes.stream() | 241 | out.println("" + guessTypes.stream() |
@@ -1,9 +1,11 @@ | |||
1 | <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | 1 | <project xmlns="http://maven.apache.org/POM/4.0.0" |
2 | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
3 | xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
2 | <modelVersion>4.0.0</modelVersion> | 4 | <modelVersion>4.0.0</modelVersion> |
3 | 5 | ||
4 | <groupId>com.github.markozajc</groupId> | 6 | <groupId>com.github.markozajc</groupId> |
5 | <artifactId>akiwrapper</artifactId> | 7 | <artifactId>akiwrapper</artifactId> |
6 | <version>1.5.2-2</version> | 8 | <version>1.6</version> |
7 | 9 | ||
8 | <name>Akiwrapper</name> | 10 | <name>Akiwrapper</name> |
9 | <description>A Java API wrapper for Akinator</description> | 11 | <description>A Java API wrapper for Akinator</description> |
@@ -29,13 +31,14 @@ | |||
29 | <scm> | 31 | <scm> |
30 | <url>https://git.zajc.eu.org/akiwrapper.git/</url> | 32 | <url>https://git.zajc.eu.org/akiwrapper.git/</url> |
31 | <connection>scm:git:https:https://git.zajc.eu.org/akiwrapper.git/</connection> | 33 | <connection>scm:git:https:https://git.zajc.eu.org/akiwrapper.git/</connection> |
32 | <developerConnection>scm:git:ssh://git@zajc.eu.org/srv/git/akiwrapper.git</developerConnection> | 34 | <developerConnection> |
35 | scm:git:ssh://git@zajc.eu.org/srv/git/akiwrapper.git</developerConnection> | ||
33 | </scm> | 36 | </scm> |
34 | 37 | ||
35 | <properties> | 38 | <properties> |
36 | <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> | 39 | <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> |
37 | <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> | 40 | <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> |
38 | <junit.version>5.9.3</junit.version> | 41 | <junit.version>5.10.0</junit.version> |
39 | <slf4j.version>2.0.7</slf4j.version> | 42 | <slf4j.version>2.0.7</slf4j.version> |
40 | <maven.compiler.source>11</maven.compiler.source> | 43 | <maven.compiler.source>11</maven.compiler.source> |
41 | <maven.compiler.target>11</maven.compiler.target> | 44 | <maven.compiler.target>11</maven.compiler.target> |
@@ -47,21 +50,21 @@ | |||
47 | <dependency> | 50 | <dependency> |
48 | <groupId>com.jcabi</groupId> | 51 | <groupId>com.jcabi</groupId> |
49 | <artifactId>jcabi-xml</artifactId> | 52 | <artifactId>jcabi-xml</artifactId> |
50 | <version>0.27.2</version> | 53 | <version>0.29.0</version> |
51 | </dependency> | 54 | </dependency> |
52 | 55 | ||
53 | <!-- JSON parsing --> | 56 | <!-- JSON parsing --> |
54 | <dependency> | 57 | <dependency> |
55 | <groupId>org.json</groupId> | 58 | <groupId>org.json</groupId> |
56 | <artifactId>json</artifactId> | 59 | <artifactId>json</artifactId> |
57 | <version>20230227</version> | 60 | <version>20230618</version> |
58 | </dependency> | 61 | </dependency> |
59 | 62 | ||
60 | <!-- HTTP requests --> | 63 | <!-- HTTP requests --> |
61 | <dependency> | 64 | <dependency> |
62 | <groupId>com.konghq</groupId> | 65 | <groupId>com.konghq</groupId> |
63 | <artifactId>unirest-java</artifactId> | 66 | <artifactId>unirest-java</artifactId> |
64 | <version>3.14.2</version> | 67 | <version>3.14.5</version> |
65 | </dependency> | 68 | </dependency> |
66 | 69 | ||
67 | <!-- Logging --> | 70 | <!-- Logging --> |
@@ -97,6 +100,13 @@ | |||
97 | <scope>test</scope> | 100 | <scope>test</scope> |
98 | </dependency> | 101 | </dependency> |
99 | 102 | ||
103 | <!-- Collections --> | ||
104 | <dependency> | ||
105 | <groupId>org.eclipse.collections</groupId> | ||
106 | <artifactId>eclipse-collections</artifactId> | ||
107 | <version>11.1.0</version> | ||
108 | </dependency> | ||
109 | |||
100 | <!-- Annotations --> | 110 | <!-- Annotations --> |
101 | <dependency> | 111 | <dependency> |
102 | <groupId>com.github.spotbugs</groupId> | 112 | <groupId>com.github.spotbugs</groupId> |
diff --git a/src/main/java/com/github/markozajc/akiwrapper/Akiwrapper.java b/src/main/java/com/github/markozajc/akiwrapper/Akiwrapper.java index 6480f12..2a979da 100644 --- a/src/main/java/com/github/markozajc/akiwrapper/Akiwrapper.java +++ b/src/main/java/com/github/markozajc/akiwrapper/Akiwrapper.java | |||
@@ -2,15 +2,44 @@ package com.github.markozajc.akiwrapper; | |||
2 | 2 | ||
3 | import static java.util.stream.Collectors.toList; | 3 | import static java.util.stream.Collectors.toList; |
4 | 4 | ||
5 | import java.io.ObjectInputFilter.Status; | ||
5 | import java.util.List; | 6 | import java.util.List; |
6 | 7 | ||
7 | import javax.annotation.*; | 8 | import javax.annotation.*; |
8 | 9 | ||
9 | import com.github.markozajc.akiwrapper.core.entities.*; | 10 | import com.github.markozajc.akiwrapper.core.entities.*; |
11 | import com.github.markozajc.akiwrapper.core.entities.Server.*; | ||
12 | import com.github.markozajc.akiwrapper.core.exceptions.*; | ||
10 | 13 | ||
11 | /** | 14 | /** |
12 | * The "core" of interaction with the Akinator's API. Contains all methods required | 15 | * The "core" of interaction with the Akinator's API.<br> |
13 | * to fully utilize all (known) Akinator's API's endpoints. | 16 | * Akinator is a 20 questions-type game, which means that the computer |
17 | * algorithmically tries to figure out (using {@link Guess}es) what character, | ||
18 | * object, or movie the player is thinking about by asking {@link Question}s and | ||
19 | * getting {@link Answer}s. As mentioned before, this library interfaces with | ||
20 | * Akinator's online API and does not run the ranking algorithm locally.<br> | ||
21 | * <br> | ||
22 | * The library is typically used in the following loop: | ||
23 | * <ol> | ||
24 | * <li>The API sends a question, which is retrieved using {@link #getQuestion()} and | ||
25 | * shown to the player</li> | ||
26 | * <li>The player gives one of the five {@link Answer}s, which is submitted using | ||
27 | * {@link #answer(Answer)} and returns the next question</li> | ||
28 | * <li>Before displaying the next question, {@link #suggestGuess()} is called to | ||
29 | * fetch the most relevant {@link Guess} (or {@code null} if none is available)</li> | ||
30 | * </ol> | ||
31 | * If a {@link Guess} is available, the following should happen: | ||
32 | * <ol> | ||
33 | * <li>The guess metadata is shown to the player | ||
34 | * <ul> | ||
35 | * <li>If the player confirms the guess, {@link #confirmGuess(Guess)} is called and | ||
36 | * the game is finished | ||
37 | * <li>If the player rejects the guess, {@link #rejectLastGuess()} is called and the | ||
38 | * game continues | ||
39 | * </ul> | ||
40 | * </ol> | ||
41 | * An example of this library in action can be viewed in the {@code examples/} | ||
42 | * directory of the repository. | ||
14 | * | 43 | * |
15 | * @author Marko Zajc | 44 | * @author Marko Zajc |
16 | */ | 45 | */ |
@@ -62,93 +91,138 @@ public interface Akiwrapper { | |||
62 | } | 91 | } |
63 | 92 | ||
64 | /** | 93 | /** |
65 | * Answers current question and retrieves the next one. The next question is passed | 94 | * Returns the API {@link Server} currently in use. The {@link Server} is |
66 | * as return value and can be retrieved later on with {@link #getQuestion()}. If | 95 | * automatically determined and set by {@link AkiwrapperBuilder} based on the |
67 | * there are no more questions left, this will return {@code null}. | 96 | * {@link Language} and {@link GuessType} preferences, both of which can be retrieved |
97 | * from the {@link Server} object itself. | ||
68 | * | 98 | * |
69 | * @param answer | 99 | * @return the API server currently in use. |
70 | * the answer | 100 | */ |
71 | * | 101 | @Nonnull |
72 | * @return the latest question or null if an answer was found | 102 | Server getServer(); |
103 | |||
104 | /** | ||
105 | * Returns whether or not Akinator has been instructed to filter out explicit | ||
106 | * content.<br> | ||
107 | * This can be configured in | ||
108 | * {@link AkiwrapperBuilder#setFilterProfanity(boolean)}.<br> | ||
109 | * Keep in mind that explicit {@link Guess}es can still be returned by | ||
110 | * {@link #getGuesses()} or {@link #suggestGuess()}, see {@link Guess#isExplicit()} | ||
111 | * for more details on why that is. | ||
73 | * | 112 | * |
74 | * @deprecated Use {@link #answer(Answer)} instead | 113 | * @return whether the profanity filter is enabled. |
75 | */ | 114 | */ |
76 | @Nullable | 115 | boolean doesFilterProfanity(); |
77 | @Deprecated(since = "1.5.2", forRemoval = true) | ||
78 | default Question answerCurrentQuestion(Answer answer) { | ||
79 | return answer(answer); | ||
80 | } | ||
81 | 116 | ||
82 | /** | 117 | /** |
83 | * Answers current question and retrieves the next one. The next question is passed | 118 | * Sends an answer to the current {@link Question} and fetches the next one, |
84 | * as return value and can be retrieved later on with {@link #getQuestion()}. If | 119 | * incrementing the current step.<br> |
85 | * there are no more questions left, this will return {@code null}. | 120 | * If there are no more questions left, this will return {@code null}. Any subsequent |
121 | * calls to this method after the question list has been exhausted will throw a | ||
122 | * {@link QuestionsExhaustedException}. A call to this method can be undone with | ||
123 | * {@link #undoAnswer()}. | ||
86 | * | 124 | * |
87 | * @param answer | 125 | * @param answer |
88 | * the answer | 126 | * the {@link Answer} to send. |
127 | * | ||
128 | * @return the next {@link Question} or {@code null} if there are no questions left. | ||
129 | * | ||
130 | * @throws QuestionsExhaustedException | ||
131 | * if the session has exhausted all questions (when | ||
132 | * {@link #isExhausted()} returns {@code true}). | ||
133 | * @throws ServerStatusException | ||
134 | * if the API server returns an erroneous {@link Status}. | ||
89 | * | 135 | * |
90 | * @return the latest question or null if an answer was found | 136 | * @see #undoAnswer() |
91 | */ | 137 | */ |
92 | @Nullable | 138 | @Nullable |
93 | Question answer(Answer answer); | 139 | Question answer(Answer answer); |
94 | 140 | ||
95 | /** | 141 | /** |
96 | * Goes one question backwards.<br> | 142 | * Goes one question backwards, undoing the previous {@link #answer(Answer)} call. |
97 | * For example, if {@link #getQuestion()} returns a question on step {@code 5}, | 143 | * For example, if {@link #getQuestion()} returns a question on step {@code 5}, |
98 | * calling this command will make {@link #getQuestion()} return the question from | 144 | * calling this command will make {@link #getQuestion()} return the question from |
99 | * step {@code 4}. You can call this as many times as you want.<br> | 145 | * step {@code 4}. You can call this as many times as you want, until you reach step |
100 | * <strong> Beware that calling this when {@link #getQuestion()} returns a question | 146 | * {@code 0}<br> |
101 | * on step {@code 0}, calling this will return {@code null} and nothing will actually | 147 | * If this method is called on step {@code 0}, {@link UndoOutOfBoundsException} is |
102 | * be changed!<br> | 148 | * thrown. If this method is called after questions have been exhausted, |
103 | * This will also return {@code null} if {@link #getQuestion()} returns {@code null} | 149 | * {@link QuestionsExhaustedException} is thrown. |
104 | * as well.</strong> | 150 | * |
105 | * | 151 | * @return the previous {@link Question}. |
106 | * @return the past message | 152 | * |
153 | * @throws UndoOutOfBoundsException | ||
154 | * if the session has exhausted all questions (when | ||
155 | * {@link #getQuestion()} returns {@code null}. | ||
156 | * @throws QuestionsExhaustedException | ||
157 | * if the session has exhausted all questions (when | ||
158 | * {@link #isExhausted()} returns {@code true}). | ||
159 | * @throws ServerStatusException | ||
160 | * if the API server returns an erroneous {@link Status}. | ||
161 | * | ||
162 | * @see #answer(Answer) | ||
107 | */ | 163 | */ |
108 | @Nullable | 164 | @Nonnull |
109 | Question undoAnswer(); | 165 | Question undoAnswer(); |
110 | 166 | ||
111 | /** | 167 | /** |
112 | * Returns the current question. You can answer it with {@link #answer(Answer)}. If | 168 | * Returns a probability-sorted (the lower the index, the higher the probability) and |
113 | * there are no more questions left, this will return {@code null}. | 169 | * unmodifiable list of <b>all relevant</b> Akinator's guesses, or an empty list if |
170 | * there are no guesses. | ||
171 | * | ||
172 | * @return a sorted list of {@link Guess}es. | ||
114 | * | 173 | * |
115 | * @return current question | 174 | * @see Akiwrapper#getGuesses(int) |
116 | * | 175 | * |
117 | * @deprecated Use {@link #getQuestion()} instead | 176 | * @throws ServerStatusException |
177 | * if the API server returns an erroneous {@link Status}. | ||
118 | */ | 178 | */ |
119 | @Nullable | 179 | @Nonnull |
120 | @Deprecated(since = "1.5.2", forRemoval = true) | 180 | default List<Guess> getGuesses() { |
121 | default Question getCurrentQuestion() { | 181 | return getGuesses(0); |
122 | return getQuestion(); | ||
123 | } | 182 | } |
124 | 183 | ||
125 | /** | 184 | /** |
126 | * Returns the current question. You can answer it with {@link #answer(Answer)}. If | 185 | * Returns a probability-sorted (the lower the index, the higher the probability) and |
127 | * there are no more questions left, this will return {@code null}. | 186 | * unmodifiable list of <b>the first N</b> Akinator's guesses, or an empty list if |
187 | * there are no guesses.<br> | ||
188 | * Note that the API may return less guesses than requested, but not more. | ||
189 | * | ||
190 | * @param count | ||
191 | * the number of {@link Guess}es to get. | ||
192 | * | ||
193 | * @return a sorted list of {@link Guess}es. | ||
194 | * | ||
195 | * @see Akiwrapper#getGuesses() | ||
196 | */ | ||
197 | @Nonnull | ||
198 | List<Guess> getGuesses(int count); | ||
199 | |||
200 | /** | ||
201 | * Returns the current {@link Question} or {@code null} if the question list has been | ||
202 | * exhausted.<br> | ||
203 | * You can answer it with {@link #answer(Answer)}. | ||
128 | * | 204 | * |
129 | * @return current question | 205 | * @return the current question. |
130 | */ | 206 | */ |
131 | @Nullable | 207 | @Nullable |
132 | Question getQuestion(); | 208 | Question getQuestion(); |
133 | 209 | ||
134 | /** | 210 | /** |
135 | * Returns a probability-sorted (the lower the index, the higher the probability) and | 211 | * Returns the current step / question number. Keep in mind that this value is |
136 | * unmodifiable list of Akinator's guesses, or an empty list if there are no guesses. | 212 | * zero-based, so the first question is on step {@code 0}. |
137 | * Note that this method caches the result, which means subsequent calls will not | ||
138 | * make API requests. | ||
139 | * | 213 | * |
140 | * @return a sorted list of guesses | 214 | * @return the current step |
141 | * | ||
142 | * @see Akiwrapper#getGuessesAboveProbability(double) | ||
143 | */ | 215 | */ |
144 | @Nonnull | 216 | int getStep(); |
145 | List<Guess> getGuesses(); | ||
146 | 217 | ||
147 | /** | 218 | /** |
148 | * @return the API server this instance of Akiwrapper uses. | 219 | * Returns if the session has been exhausted, which occurs after answering 80 |
220 | * {@link Question}s.<br> | ||
221 | * Sending or undoing answers can no longer be done after the session is exhausted. | ||
222 | * | ||
223 | * @return whether the session is exhausted. | ||
149 | */ | 224 | */ |
150 | @Nonnull | 225 | boolean isExhausted(); |
151 | Server getServer(); | ||
152 | 226 | ||
153 | /** | 227 | /** |
154 | * @param probability | 228 | * @param probability |
@@ -158,10 +232,66 @@ public interface Akiwrapper { | |||
158 | * probability threshold. | 232 | * probability threshold. |
159 | * | 233 | * |
160 | * @see Akiwrapper#getGuesses() | 234 | * @see Akiwrapper#getGuesses() |
235 | * | ||
236 | * @deprecated Use {@link #suggestGuess()} instead | ||
161 | */ | 237 | */ |
162 | @SuppressWarnings("null") | ||
163 | @Nonnull | 238 | @Nonnull |
239 | @SuppressWarnings("null") | ||
240 | @Deprecated(since = "1.6", forRemoval = true) | ||
164 | default List<Guess> getGuessesAboveProbability(double probability) { | 241 | default List<Guess> getGuessesAboveProbability(double probability) { |
165 | return getGuesses().stream().filter(g -> g.getProbability() > probability).collect(toList()); | 242 | return getGuesses().stream().filter(g -> g.getProbability() > probability).collect(toList()); |
166 | } | 243 | } |
244 | |||
245 | /** | ||
246 | * <b>Important:</b> this method mutates the session's state, which means that | ||
247 | * subsequent calls to it will not yield the same result.<br> | ||
248 | * <br> | ||
249 | * Provides a likely {@link Guess} for the current session or {@code null} if none | ||
250 | * are available. This method should be called after every call to | ||
251 | * {@link #answer(Answer)} to let the player review Akinator's guesses - the internal | ||
252 | * logic replicates how Akinator works, only returning a {@link Guess} when | ||
253 | * Akinator's confidence is high enough. It will also space suggestions out evenly, | ||
254 | * always returning {@code null} for a number of steps after the last suggestion. | ||
255 | * | ||
256 | * @return a suggested {@link Guess} or {@code null} if none are available | ||
257 | */ | ||
258 | @Nullable | ||
259 | Guess suggestGuess(); | ||
260 | |||
261 | /** | ||
262 | * Confirms a {@link Guess}. While this doesn't affect the current session, because | ||
263 | * it's called at the very end, it likely affects Akinator's algorithm and associates | ||
264 | * the taken answer route with the confirmed guess, thus improving the game for | ||
265 | * everyone. | ||
266 | * | ||
267 | * @param guess | ||
268 | * the {@link Guess} to confirm. | ||
269 | * | ||
270 | * @apiNote Do not use this method in automated tests, as it introduces faulty data | ||
271 | * into Akinator's database, dulling the ranking algorithm. | ||
272 | */ | ||
273 | void confirmGuess(@Nonnull Guess guess); | ||
274 | |||
275 | /** | ||
276 | * <b>Note:</b> this method should only be called immediately after | ||
277 | * {@link #suggestGuess()} returns a {@link Guess} - don't send or undo answers | ||
278 | * before calling it.<br> | ||
279 | * <br> | ||
280 | * Rejects the previously suggested {@link Guess} and provides an updated | ||
281 | * {@link Question} (or {@code null} if the session is exhausted). The question is on | ||
282 | * the same step that the last call to {@link #answer(Answer)} was, but might have | ||
283 | * different text. | ||
284 | * | ||
285 | * @return the replacement {@link Question}. | ||
286 | * | ||
287 | * @throws ServerStatusException | ||
288 | * if the API server returns an erroneous {@link Status}. Throwing this | ||
289 | * is suppressed if the session is already exhausted. | ||
290 | * | ||
291 | * @apiNote Do not use this method in automated tests, as it introduces faulty data | ||
292 | * into Akinator's database, dulling the ranking algorithm. | ||
293 | */ | ||
294 | @Nullable | ||
295 | Question rejectLastGuess(); | ||
296 | |||
167 | } | 297 | } |
diff --git a/src/main/java/com/github/markozajc/akiwrapper/AkiwrapperBuilder.java b/src/main/java/com/github/markozajc/akiwrapper/AkiwrapperBuilder.java index e13b31a..944668b 100644 --- a/src/main/java/com/github/markozajc/akiwrapper/AkiwrapperBuilder.java +++ b/src/main/java/com/github/markozajc/akiwrapper/AkiwrapperBuilder.java | |||
@@ -3,6 +3,7 @@ package com.github.markozajc.akiwrapper; | |||
3 | import static com.github.markozajc.akiwrapper.core.entities.Server.GuessType.CHARACTER; | 3 | import static com.github.markozajc.akiwrapper.core.entities.Server.GuessType.CHARACTER; |
4 | import static com.github.markozajc.akiwrapper.core.entities.Server.Language.ENGLISH; | 4 | import static com.github.markozajc.akiwrapper.core.entities.Server.Language.ENGLISH; |
5 | import static com.github.markozajc.akiwrapper.core.utils.Servers.findServers; | 5 | import static com.github.markozajc.akiwrapper.core.utils.Servers.findServers; |
6 | import static java.lang.String.format; | ||
6 | 7 | ||
7 | import javax.annotation.*; | 8 | import javax.annotation.*; |
8 | 9 | ||
@@ -12,19 +13,12 @@ import com.github.markozajc.akiwrapper.core.entities.*; | |||
12 | import com.github.markozajc.akiwrapper.core.entities.Server.*; | 13 | import com.github.markozajc.akiwrapper.core.entities.Server.*; |
13 | import com.github.markozajc.akiwrapper.core.exceptions.*; | 14 | import com.github.markozajc.akiwrapper.core.exceptions.*; |
14 | import com.github.markozajc.akiwrapper.core.impl.AkiwrapperImpl; | 15 | import com.github.markozajc.akiwrapper.core.impl.AkiwrapperImpl; |
15 | import com.github.markozajc.akiwrapper.core.utils.*; | 16 | import com.github.markozajc.akiwrapper.core.utils.UnirestUtils; |
16 | 17 | ||
17 | import kong.unirest.UnirestInstance; | 18 | import kong.unirest.UnirestInstance; |
18 | 19 | ||
19 | /** | 20 | /** |
20 | * A class used to build an {@link Akiwrapper} object. It allows you to set various | 21 | * A class used to build an {@link Akiwrapper} object. |
21 | * values before building it in a method chaining fashion. Note that | ||
22 | * {@link Language}, {@link GuessType}, and {@link Server} configuration are | ||
23 | * connected - {@link Language} and {@link GuessType} are used to find a suitable | ||
24 | * {@link Server}, but they will only be used if a {@link Server} is not manually | ||
25 | * set. It is not recommended to set the {@link Server} manually (unless for | ||
26 | * debugging purposes or as some kind of workaround where Akiwrapper's server finder | ||
27 | * fails) as Akiwrapper already does its best to find the most suitable one. | ||
28 | * | 22 | * |
29 | * @author Marko Zajc | 23 | * @author Marko Zajc |
30 | */ | 24 | */ |
@@ -33,7 +27,6 @@ public class AkiwrapperBuilder { | |||
33 | private static final Logger LOG = LoggerFactory.getLogger(AkiwrapperBuilder.class); | 27 | private static final Logger LOG = LoggerFactory.getLogger(AkiwrapperBuilder.class); |
34 | 28 | ||
35 | @Nullable private UnirestInstance unirest; | 29 | @Nullable private UnirestInstance unirest; |
36 | @Nullable private Server server; | ||
37 | private boolean filterProfanity; | 30 | private boolean filterProfanity; |
38 | @Nonnull private Language language; | 31 | @Nonnull private Language language; |
39 | @Nonnull private GuessType guessType; | 32 | @Nonnull private GuessType guessType; |
@@ -46,27 +39,41 @@ public class AkiwrapperBuilder { | |||
46 | /** | 39 | /** |
47 | * The default {@link Language} for new {@link Akiwrapper} instances. | 40 | * The default {@link Language} for new {@link Akiwrapper} instances. |
48 | */ | 41 | */ |
49 | @Nonnull public static final Language DEFAULT_LOCALIZATION = ENGLISH; | 42 | @Nonnull public static final Language DEFAULT_LANGUAGE = ENGLISH; |
43 | |||
44 | /** | ||
45 | * The default {@link Language} for new {@link Akiwrapper} instances. | ||
46 | * | ||
47 | * @deprecated Use {@link #DEFAULT_LANGUAGE} instead | ||
48 | */ | ||
49 | @Deprecated(since = "1.6", forRemoval = true) | ||
50 | @Nonnull public static final Language DEFAULT_LOCALIZATION = DEFAULT_LANGUAGE; | ||
50 | 51 | ||
51 | /** | 52 | /** |
52 | * The default {@link GuessType} for new {@link Akiwrapper} instances. | 53 | * The default {@link GuessType} for new {@link Akiwrapper} instances. |
53 | */ | 54 | */ |
54 | @Nonnull public static final GuessType DEFAULT_GUESS_TYPE = CHARACTER; | 55 | @Nonnull public static final GuessType DEFAULT_GUESS_TYPE = CHARACTER; |
55 | 56 | ||
56 | private AkiwrapperBuilder(@Nullable UnirestInstance unirest, @Nullable Server server, boolean filterProfanity, | 57 | private AkiwrapperBuilder(@Nullable UnirestInstance unirest, boolean filterProfanity, @Nonnull Language language, |
57 | @Nonnull Language language, @Nonnull GuessType guessType) { | 58 | @Nonnull GuessType guessType) { |
58 | this.unirest = unirest; | 59 | this.unirest = unirest; |
59 | this.server = server; | ||
60 | this.filterProfanity = filterProfanity; | 60 | this.filterProfanity = filterProfanity; |
61 | this.language = language; | 61 | this.language = language; |
62 | this.guessType = guessType; | 62 | this.guessType = guessType; |
63 | } | 63 | } |
64 | 64 | ||
65 | /** | 65 | /** |
66 | * Creates a new AkiwrapperBuilder object. | 66 | * Creates a new {@link AkiwrapperBuilder} with the following defaults: |
67 | * <ul> | ||
68 | * <li>profanity filtering is set to {@code false} | ||
69 | * ({@link #DEFAULT_FILTER_PROFANITY}), | ||
70 | * <li>language is set to {@link Language#ENGLISH} ({@link #DEFAULT_LANGUAGE}), | ||
71 | * <li>guess type is set to {@link GuessType#CHARACTER} | ||
72 | * ({@link #DEFAULT_GUESS_TYPE}), | ||
73 | * </ul> | ||
67 | */ | 74 | */ |
68 | public AkiwrapperBuilder() { | 75 | public AkiwrapperBuilder() { |
69 | this(null, null, DEFAULT_FILTER_PROFANITY, DEFAULT_LOCALIZATION, DEFAULT_GUESS_TYPE); | 76 | this(null, DEFAULT_FILTER_PROFANITY, DEFAULT_LANGUAGE, DEFAULT_GUESS_TYPE); |
70 | } | 77 | } |
71 | 78 | ||
72 | /** | 79 | /** |
@@ -76,7 +83,8 @@ public class AkiwrapperBuilder { | |||
76 | * {@link UnirestUtils#configureInstance(UnirestInstance)} before using it with | 83 | * {@link UnirestUtils#configureInstance(UnirestInstance)} before using it with |
77 | * Akiwrapper. You will also need to shut it down yourself, or, if you decide to set | 84 | * Akiwrapper. You will also need to shut it down yourself, or, if you decide to set |
78 | * or leave this on {$code null}, call {@link UnirestUtils#shutdownInstance()} to | 85 | * or leave this on {$code null}, call {@link UnirestUtils#shutdownInstance()} to |
79 | * shut down Akiwrapper's default singleton instance. | 86 | * shut down Akiwrapper's default singleton instance, otherwise its threads will stay |
87 | * alive. | ||
80 | * | 88 | * |
81 | * @param unirest | 89 | * @param unirest |
82 | * the {@link UnirestInstance} to be used by Akiwrapper or {$code null} to | 90 | * the {@link UnirestInstance} to be used by Akiwrapper or {$code null} to |
@@ -96,7 +104,7 @@ public class AkiwrapperBuilder { | |||
96 | * Returns the {@link UnirestInstance} to be used by the built Akiwrapper instance. | 104 | * Returns the {@link UnirestInstance} to be used by the built Akiwrapper instance. |
97 | * If this is {$code null}, {@link UnirestUtils#getInstance()} will be used (which | 105 | * If this is {$code null}, {@link UnirestUtils#getInstance()} will be used (which |
98 | * means you will need to shut it down through | 106 | * means you will need to shut it down through |
99 | * {@link UnirestUtils#shutdownInstance()}). | 107 | * {@link UnirestUtils#shutdownInstance()}, otherwise its threads will stay alive). |
100 | * | 108 | * |
101 | * @return {@link UnirestInstance} to be used or {$code null} for | 109 | * @return {@link UnirestInstance} to be used or {$code null} for |
102 | * {@link UnirestUtils#getInstance()} | 110 | * {@link UnirestUtils#getInstance()} |
@@ -107,47 +115,15 @@ public class AkiwrapperBuilder { | |||
107 | } | 115 | } |
108 | 116 | ||
109 | /** | 117 | /** |
110 | * Sets the {@link Server} or (recommended) a {@link ServerList}. It is not | 118 | * Sets the "filter profanity" mode. Keep in mind that explicit {@link Guess}es can |
111 | * recommended to set the {@link Server} manually (unless for debugging purposes or | 119 | * still be returned by {@link Akiwrapper#getGuesses()} or |
112 | * as some kind of workaround where Akiwrapper's server finder fails) as Akiwrapper | 120 | * {@link Akiwrapper#suggestGuess()}, see {@link Guess#isExplicit()} for more details |
113 | * already does its best to find the most suitable one. <br> | 121 | * on why that is.<br> |
114 | * <b>Caution!</b> Setting the server to a non-null value overwrites the | 122 | * This is set to {@code false} by default. |
115 | * {@link Language} and the {@link GuessType} with the given {@link Server}'s values. | ||
116 | * | ||
117 | * @param server | ||
118 | * | ||
119 | * @return current instance, used for chaining | ||
120 | * | ||
121 | * @see #getServer() | ||
122 | * @see Servers#findServers(UnirestInstance, Language, GuessType) | ||
123 | */ | ||
124 | @Nonnull | ||
125 | public AkiwrapperBuilder setServer(@Nullable Server server) { | ||
126 | this.server = server; | ||
127 | if (server != null) { | ||
128 | this.language = server.getLanguage(); | ||
129 | this.guessType = server.getGuessType(); | ||
130 | } | ||
131 | return this; | ||
132 | } | ||
133 | |||
134 | /** | ||
135 | * Returns the {@link Server} that requests will be sent to. Might also return a | ||
136 | * {@link ServerList} (which extends {@link Server}). | ||
137 | * | ||
138 | * @return server. | ||
139 | */ | ||
140 | @Nullable | ||
141 | public Server getServer() { | ||
142 | return this.server; | ||
143 | } | ||
144 | |||
145 | /** | ||
146 | * Sets the "filter profanity" mode. | ||
147 | * | 123 | * |
148 | * @param filterProfanity | 124 | * @param filterProfanity |
149 | * | 125 | * |
150 | * @return current instance, used for chaining | 126 | * @return current instance, used for chaining. |
151 | * | 127 | * |
152 | * @see #doesFilterProfanity() | 128 | * @see #doesFilterProfanity() |
153 | */ | 129 | */ |
@@ -159,7 +135,10 @@ public class AkiwrapperBuilder { | |||
159 | 135 | ||
160 | /** | 136 | /** |
161 | * Returns the profanity filter preference. Profanity filtering is done by Akinator | 137 | * Returns the profanity filter preference. Profanity filtering is done by Akinator |
162 | * and not by Akiwrapper. | 138 | * and not by Akiwrapper. Keep in mind that explicit {@link Guess}es can still be |
139 | * returned by {@link Akiwrapper#getGuesses()} or {@link Akiwrapper#suggestGuess()}, | ||
140 | * see {@link Guess#isExplicit()} for more details on why that is.<br> | ||
141 | * This is set to {@code false} by default. | ||
163 | * | 142 | * |
164 | * @return profanity filter preference. | 143 | * @return profanity filter preference. |
165 | */ | 144 | */ |
@@ -168,28 +147,31 @@ public class AkiwrapperBuilder { | |||
168 | } | 147 | } |
169 | 148 | ||
170 | /** | 149 | /** |
171 | * Sets the {@link Language}.<br> | 150 | * <b>Note:</b> not all {@link Language}s support all {@link GuessType}s. The |
172 | * <b>Caution!</b> Setting the {@link Language} will set the {@link Server} to | 151 | * standard ones seem to be {@link GuessType#ANIMAL}, {@link GuessType#CHARACTER}, |
173 | * {@code null} (meaning it will be automatically selected). | 152 | * and {@link GuessType#OBJECT}, but you might still face |
153 | * {@link ServerNotFoundException}s using them or other ones.<br> | ||
154 | * <br> | ||
155 | * Sets the {@link Language}. The server will return localized {@link Question}s and | ||
156 | * {@link Guess}es depending on this preference.<br> | ||
157 | * This is set to {@link Language#ENGLISH} by default. | ||
174 | * | 158 | * |
175 | * @param language | 159 | * @param language |
176 | * | 160 | * |
177 | * @return current instance, used for chaining | 161 | * @return current instance, used for chaining. |
178 | * | 162 | * |
179 | * @see #getLanguage() | 163 | * @see #getLanguage() |
180 | */ | 164 | */ |
181 | @Nonnull | 165 | @Nonnull |
182 | public AkiwrapperBuilder setLanguage(@Nonnull Language language) { | 166 | public AkiwrapperBuilder setLanguage(@Nonnull Language language) { |
183 | this.language = language; | 167 | this.language = language; |
184 | this.server = null; | ||
185 | return this; | 168 | return this; |
186 | } | 169 | } |
187 | 170 | ||
188 | /** | 171 | /** |
189 | * Returns the {@link Language} preference. {@link Language} impacts what language | 172 | * Returns the {@link Language}. The server will return localized {@link Question}s |
190 | * {@link Question}s and {@link Guess}es are in.<br> | 173 | * and {@link Guess}es depending on this preference.<br> |
191 | * {@link #getGuessType()} and {@link #getLanguage()} decide what {@link Server} will | 174 | * This is set to {@link Language#ENGLISH} by default. |
192 | * be used if it's not set manually. | ||
193 | * | 175 | * |
194 | * @return language preference. | 176 | * @return language preference. |
195 | */ | 177 | */ |
@@ -199,20 +181,25 @@ public class AkiwrapperBuilder { | |||
199 | } | 181 | } |
200 | 182 | ||
201 | /** | 183 | /** |
202 | * Sets the {@link GuessType}.<br> | 184 | * <b>Note:</b> not all {@link Language}s support all {@link GuessType}s. The |
203 | * <b>Caution!</b> Setting the {@link Language} will set the {@link Server} to | 185 | * standard ones seem to be {@link GuessType#ANIMAL}, {@link GuessType#CHARACTER}, |
204 | * {@code null} (meaning it will be automatically selected). | 186 | * and {@link GuessType#OBJECT}, but you might still face |
187 | * {@link ServerNotFoundException}s using them or other ones.<br> | ||
188 | * <br> | ||
189 | * Sets the {@link GuessType}. This decides what kind of things the {@link Server}'s | ||
190 | * {@link Guess}es will represent. While the name might imply that this affects only | ||
191 | * guess content, it also affects {@link Question}s.<br> | ||
192 | * This is set to {@link GuessType#CHARACTER} by default. | ||
205 | * | 193 | * |
206 | * @param guessType | 194 | * @param guessType |
207 | * | 195 | * |
208 | * @return current instance, used for chaining | 196 | * @return current instance, used for chaining. |
209 | * | 197 | * |
210 | * @see #getLanguage() | 198 | * @see #getLanguage() |
211 | */ | 199 | */ |
212 | @Nonnull | 200 | @Nonnull |
213 | public AkiwrapperBuilder setGuessType(@Nonnull GuessType guessType) { | 201 | public AkiwrapperBuilder setGuessType(@Nonnull GuessType guessType) { |
214 | this.guessType = guessType; | 202 | this.guessType = guessType; |
215 | this.server = null; | ||
216 | return this; | 203 | return this; |
217 | } | 204 | } |
218 | 205 | ||
@@ -220,7 +207,8 @@ public class AkiwrapperBuilder { | |||
220 | * Returns the {@link GuessType} preference. {@link GuessType} impacts what kind of | 207 | * Returns the {@link GuessType} preference. {@link GuessType} impacts what kind of |
221 | * subject {@link Question}s and {@link Guess}es are about.<br> | 208 | * subject {@link Question}s and {@link Guess}es are about.<br> |
222 | * {@link #getGuessType()} and {@link #getLanguage()} decide what {@link Server} will | 209 | * {@link #getGuessType()} and {@link #getLanguage()} decide what {@link Server} will |
223 | * be used if it's not set manually. | 210 | * be used if it's not set manually.<br> |
211 | * This is set to {@link GuessType#CHARACTER} by default. | ||
224 | * | 212 | * |
225 | * @return guess type preference. | 213 | * @return guess type preference. |
226 | */ | 214 | */ |
@@ -234,42 +222,37 @@ public class AkiwrapperBuilder { | |||
234 | * {@link UnirestInstance} was set (with | 222 | * {@link UnirestInstance} was set (with |
235 | * {@link #setUnirestInstance(UnirestInstance)}), a singleton instance will be | 223 | * {@link #setUnirestInstance(UnirestInstance)}), a singleton instance will be |
236 | * acquired from {@link UnirestUtils#getInstance()}. This instance must be shut down | 224 | * acquired from {@link UnirestUtils#getInstance()}. This instance must be shut down |
237 | * after you're done using Akiwrapper with {@link UnirestUtils#shutdownInstance()}. | 225 | * after you're done using Akiwrapper with {@link UnirestUtils#shutdownInstance()}, |
238 | * If no server was set (with {@link #setServer(Server)}), Akiwrapper will find one | 226 | * otherwise its threads will stay alive. |
239 | * based on {@link #getLanguage()} and {@link #getGuessType()} for you. | ||
240 | * | 227 | * |
241 | * @return a new {@link Akiwrapper} instance that will use all set preferences | 228 | * @return a new {@link Akiwrapper} instance. |
242 | * | 229 | * |
243 | * @throws ServerNotFoundException | 230 | * @throws ServerNotFoundException |
244 | * if no server with that {@link Language} and {@link GuessType} is | 231 | * if no server with that {@link Language} and {@link GuessType} is |
245 | * available. | 232 | * available. |
246 | */ | 233 | */ |
247 | @Nonnull | 234 | @Nonnull |
248 | @SuppressWarnings("resource") | 235 | @SuppressWarnings({ "resource", "null" }) |
249 | public Akiwrapper build() throws ServerNotFoundException { | 236 | public Akiwrapper build() throws ServerNotFoundException { |
250 | UnirestInstance unirest = this.unirest != null ? this.unirest : UnirestUtils.getInstance(); | 237 | var unirest = this.unirest != null ? this.unirest : UnirestUtils.getInstance(); |
251 | 238 | ||
252 | var server = this.server != null ? this.server : findServers(unirest, this.getLanguage(), this.getGuessType()); | 239 | var servers = findServers(unirest, this.getLanguage(), this.getGuessType()); |
253 | if (server instanceof ServerList) { | 240 | if (servers.isEmpty()) |
254 | ServerList serverList = (ServerList) server; | 241 | throw new ServerNotFoundException(format("No servers exist for %s - %s", this.language, this.guessType)); |
255 | int count = serverList.getRemainingSize() + 1; | ||
256 | do { | ||
257 | LOG.debug("Using server {} out of {} from the list.", count - serverList.getRemainingSize(), count); | ||
258 | try { | ||
259 | return new AkiwrapperImpl(unirest, server, this.filterProfanity); | ||
260 | 242 | ||
261 | } catch (ServerUnavailableException e) { | 243 | for (var server : servers) { |
262 | LOG.debug("Server seems to be down."); | 244 | try { |
245 | var api = new AkiwrapperImpl(unirest, server, this.filterProfanity); | ||
246 | api.createSession(); | ||
247 | return api; | ||
263 | 248 | ||
264 | } catch (RuntimeException e) { | 249 | } catch (ServerStatusException e) { |
265 | LOG.warn("Failed to construct an instance, trying the next available server", e); | 250 | LOG.debug("Failed to construct an instance, trying the next available server", e); |
266 | } | 251 | } |
267 | } while (serverList.next()); | ||
268 | throw new ServerUnavailableException("KO - NO SERVER AVAILABLE"); | ||
269 | } else { | ||
270 | LOG.debug("Given Server is not a ServerList, only attempting to build once."); | ||
271 | return new AkiwrapperImpl(unirest, server, this.filterProfanity); | ||
272 | } | 252 | } |
253 | |||
254 | throw new ServerNotFoundException(format("Servers exist for %s - %s, but none of them is usable", this.language, | ||
255 | this.guessType)); | ||
273 | } | 256 | } |
274 | 257 | ||
275 | } | 258 | } |
diff --git a/src/main/java/com/github/markozajc/akiwrapper/core/Route.java b/src/main/java/com/github/markozajc/akiwrapper/core/Route.java deleted file mode 100644 index 309e297..0000000 --- a/src/main/java/com/github/markozajc/akiwrapper/core/Route.java +++ /dev/null | |||
@@ -1,230 +0,0 @@ | |||
1 | package com.github.markozajc.akiwrapper.core; | ||
2 | |||
3 | import static com.github.markozajc.akiwrapper.core.entities.Status.Level.ERROR; | ||
4 | import static com.github.markozajc.akiwrapper.core.entities.impl.immutable.ApiKey.accquireApiKey; | ||
5 | import static java.lang.String.format; | ||
6 | import static java.lang.System.currentTimeMillis; | ||
7 | import static java.nio.charset.StandardCharsets.UTF_8; | ||
8 | import static java.util.regex.Pattern.compile; | ||
9 | |||
10 | import java.net.URLEncoder; | ||
11 | import java.util.regex.*; | ||
12 | |||
13 | import javax.annotation.*; | ||
14 | |||
15 | import org.json.*; | ||
16 | import org.slf4j.*; | ||
17 | |||
18 | import com.github.markozajc.akiwrapper.core.entities.Status; | ||
19 | import com.github.markozajc.akiwrapper.core.entities.impl.immutable.StatusImpl; | ||
20 | import com.github.markozajc.akiwrapper.core.exceptions.*; | ||
21 | import com.github.markozajc.akiwrapper.core.impl.AkiwrapperImpl.Session; | ||
22 | |||
23 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; | ||
24 | import kong.unirest.UnirestInstance; | ||
25 | |||
26 | public final class Route { | ||
27 | |||
28 | public static final String BASE_AKINATOR_URL = "https://en.akinator.com"; | ||
29 | private static final String SERVER_DOWN_STATUS_MESSAGE = "server down"; | ||
30 | private static final Pattern FILTER_ARGUMENT_PATTERN = compile("\\{FILTER\\}"); | ||
31 | |||
32 | /** | ||
33 | * Whether to run status checks on {@link Request#getJSON()} by default. Setting this | ||
34 | * to false may result in unpredicted exceptions! <b>You usually don't need to alter | ||
35 | * this value</b> | ||
36 | */ | ||
37 | @SuppressFBWarnings("MS_SHOULD_BE_FINAL") public static boolean defaultRunChecks = true; // NOSONAR | ||
38 | |||
39 | /** | ||
40 | * Creates a new session for further gameplay.<br> | ||
41 | * <b>Caution!</b> Because this endpoint uses a static hostname, you <u>must</u> pass | ||
42 | * an empty string to {@code baseUrl} of | ||
43 | * {@link Route#createRequest(UnirestInstance, String, boolean, Object...)} <br> | ||
44 | * Parameters: | ||
45 | * <ol> | ||
46 | * <li>Current time in milliseconds</li> | ||
47 | * <li>API server's URL</li> | ||
48 | * </ol> | ||
49 | */ | ||
50 | public static final Route NEW_SESSION = | ||
51 | new Route(1, | ||
52 | BASE_AKINATOR_URL + | ||
53 | "/new_session?partner=1&player=website-desktop&constraint=ETAT%%3C%%3E%%27AV%%27&{API_KEY}" + | ||
54 | "&soft_constraint={FILTER}&question_filter={FILTER}&_=%s&urlApiWs=%s", | ||
55 | "ETAT=%%27EN%%27", "cat=1"); | ||
56 | |||
57 | /** | ||
58 | * Answers a question. Parameters: | ||
59 | * <ol> | ||
60 | * <li>Current step</li> | ||
61 | * <li>Answer's ID</li> | ||
62 | * </ol> | ||
63 | */ | ||
64 | public static final Route ANSWER = new Route(2, "/answer?step=%s&answer=%s", "&question_filter=cat=1"); | ||
65 | |||
66 | /** | ||
67 | * Cancels (undoes) an answer. Parameters: | ||
68 | * <ol> | ||
69 | * <li>Current step</li> | ||
70 | * </ol> | ||
71 | */ | ||
72 | public static final Route CANCEL_ANSWER = | ||
73 | new Route(1, "/cancel_answer?step=%s&answer=-1", "&question_filter=cat=1"); | ||
74 | |||
75 | /** | ||
76 | * Lists all available guesses. Parameters: | ||
77 | * <ol> | ||
78 | * <li>Current step</li> | ||
79 | * </ol> | ||
80 | */ | ||
81 | public static final Route LIST = new Route(1, "/list?mode_question=0&step=%s"); | ||
82 | |||
83 | @Nonnull private final String path; | ||
84 | @Nonnull private final String[] filterArguments; | ||
85 | |||
86 | private final int parametersQuantity; | ||
87 | |||
88 | private Route(int parameters, @Nonnull String path) { | ||
89 | this(parameters, path, new String[0]); | ||
90 | } | ||
91 | |||
92 | private Route(int parameters, @Nonnull String path, @Nonnull String... filterArguments) { | ||
93 | this.path = path; | ||
94 | this.filterArguments = filterArguments.clone(); | ||
95 | this.parametersQuantity = parameters; | ||
96 | } | ||
97 | |||
98 | /** | ||
99 | * @deprecated Use | ||
100 | * Route#createRequest(UnirestInstance,String,boolean,Session,String...) | ||
101 | * instead | ||
102 | */ | ||
103 | @Nonnull | ||
104 | @Deprecated(since = "1.5.2", forRemoval = true) | ||
105 | public Request getRequest(@Nonnull UnirestInstance unirest, @Nonnull String baseUrl, boolean filterProfanity, | ||
106 | @Nullable Session token, @Nonnull String... parameters) { | ||
107 | return createRequest(unirest, baseUrl, filterProfanity, token, (Object[]) parameters); | ||
108 | } | ||
109 | |||
110 | @Nonnull | ||
111 | public Request createRequest(@Nonnull UnirestInstance unirest, @Nonnull String baseUrl, boolean filterProfanity, | ||
112 | @Nullable Session token, @Nonnull Object... parameters) { | ||
113 | if (parameters.length < this.parametersQuantity) | ||
114 | throw new IllegalArgumentException("Insufficient parameters; Expected " + this.parametersQuantity + | ||
115 | ", got " + | ||
116 | parameters.length); | ||
117 | |||
118 | String[] encodedParams = new String[parameters.length]; | ||
119 | for (int i = 0; i < parameters.length; i++) { | ||
120 | encodedParams[i] = URLEncoder.encode(parameters[i].toString(), UTF_8); | ||
121 | } | ||
122 | |||
123 | String formattedPath = this.path; | ||
124 | |||
125 | Matcher matcher = FILTER_ARGUMENT_PATTERN.matcher(formattedPath); | ||
126 | StringBuffer sb = new StringBuffer(); | ||
127 | for (int i = 0; matcher.find(); i++) { | ||
128 | matcher.appendReplacement(sb, filterProfanity ? this.filterArguments[i] : ""); | ||
129 | } | ||
130 | matcher.appendTail(sb); | ||
131 | formattedPath = sb.toString(); | ||
132 | formattedPath = formattedPath.replace("{API_KEY}", accquireApiKey(unirest).querystring().replace("%", "%%")); | ||
133 | formattedPath = format(formattedPath, (Object[]) encodedParams); | ||
134 | |||
135 | String jQueryCallback = "jQuery331023608747682107778_" + currentTimeMillis(); | ||
136 | formattedPath = formattedPath + "&callback=" + jQueryCallback; | ||
137 | |||
138 | if (token != null) | ||
139 | formattedPath = formattedPath + token.querystring(); | ||
140 | |||
141 | return new Request(unirest, baseUrl + formattedPath, jQueryCallback.length()); | ||
142 | } | ||
143 | |||
144 | /** | ||
145 | * @deprecated Use {@link #createRequest(UnirestInstance,String,boolean,Object...)} | ||
146 | * instead | ||
147 | */ | ||
148 | @Nonnull | ||
149 | @Deprecated(since = "1.5.2", forRemoval = true) | ||
150 | public Request getRequest(@Nonnull UnirestInstance unirest, @Nonnull String baseUrl, boolean filterProfanity, | ||
151 | @Nonnull String... parameters) { | ||
152 | return createRequest(unirest, baseUrl, filterProfanity, (Object[]) parameters); | ||
153 | } | ||
154 | |||
155 | @Nonnull | ||
156 | public Request createRequest(@Nonnull UnirestInstance unirest, @Nonnull String baseUrl, boolean filterProfanity, | ||
157 | @Nonnull Object... parameters) { | ||
158 | return this.createRequest(unirest, baseUrl, filterProfanity, null, parameters); | ||
159 | } | ||
160 | |||
161 | @Nonnull | ||
162 | public String getPath() { | ||
163 | return this.path; | ||
164 | } | ||
165 | |||
166 | /** | ||
167 | * @deprecated Use {@link #getParameterCount()} instead | ||
168 | */ | ||
169 | @Deprecated(since = "1.5.2", forRemoval = true) | ||
170 | public int getParametersQuantity() { | ||
171 | return getParameterCount(); | ||
172 | } | ||
173 | |||
174 | public int getParameterCount() { | ||
175 | return this.parametersQuantity; | ||
176 | } | ||
177 | |||
178 | public static class Request { | ||
179 | |||
180 | private static final Logger LOG = LoggerFactory.getLogger(Route.Request.class); | ||
181 | |||
182 | @Nonnull private final UnirestInstance unirest; | ||
183 | @Nonnull private final String url; | ||
184 | private final int jQueryCallbackLength; | ||
185 | |||
186 | Request(@Nonnull UnirestInstance unirest, @Nonnull String url, int jQueryCallbackLength) { | ||
187 | this.unirest = unirest; | ||
188 | this.jQueryCallbackLength = jQueryCallbackLength; | ||
189 | this.url = url; | ||
190 | } | ||
191 | |||
192 | @Nonnull | ||
193 | public JSONObject getJSON() { | ||
194 | return getJSON(defaultRunChecks); | ||
195 | } | ||
196 | |||
197 | @Nonnull | ||
198 | public JSONObject getJSON(boolean runChecks) { | ||
199 | var response = this.unirest.get(this.url).asString().getBody(); | ||
200 | response = response.substring(this.jQueryCallbackLength + 1, response.length() - 1); | ||
201 | |||
202 | LOG.trace("--> {}", this.url); | ||
203 | LOG.trace("<-- {}", response); | ||
204 | |||
205 | try { | ||
206 | var result = new JSONObject(response); | ||
207 | |||
208 | if (runChecks) | ||
209 | ensureSuccessful(result); | ||
210 | |||
211 | return result; | ||
212 | |||
213 | } catch (JSONException e) { | ||
214 | LOG.error("Failed to parse JSON from the API server", e); | ||
215 | throw new StatusException(new StatusImpl("AW-KO - COULDN'T PARSE JSON")); | ||
216 | } | ||
217 | } | ||
218 | |||
219 | static void ensureSuccessful(@Nonnull JSONObject response) { | ||
220 | Status completion = new StatusImpl(response); | ||
221 | if (completion.getLevel() == ERROR) { | ||
222 | if (SERVER_DOWN_STATUS_MESSAGE.equalsIgnoreCase(completion.getReason())) | ||
223 | throw new ServerUnavailableException(completion); | ||
224 | |||
225 | throw new StatusException(completion); | ||
226 | } | ||
227 | } | ||
228 | } | ||
229 | |||
230 | } | ||
diff --git a/src/main/java/com/github/markozajc/akiwrapper/core/entities/Guess.java b/src/main/java/com/github/markozajc/akiwrapper/core/entities/Guess.java index bc3eb3f..f3533e1 100644 --- a/src/main/java/com/github/markozajc/akiwrapper/core/entities/Guess.java +++ b/src/main/java/com/github/markozajc/akiwrapper/core/entities/Guess.java | |||
@@ -4,7 +4,7 @@ import java.net.URL; | |||
4 | 4 | ||
5 | import javax.annotation.*; | 5 | import javax.annotation.*; |
6 | 6 | ||
7 | import com.github.markozajc.akiwrapper.AkiwrapperBuilder; | 7 | import com.github.markozajc.akiwrapper.*; |
8 | import com.github.markozajc.akiwrapper.core.entities.Server.GuessType; | 8 | import com.github.markozajc.akiwrapper.core.entities.Server.GuessType; |
9 | 9 | ||
10 | /** | 10 | /** |
@@ -32,7 +32,8 @@ public interface Guess extends Identifiable, Comparable<Guess> { | |||
32 | 32 | ||
33 | /** | 33 | /** |
34 | * Returns the approximate probability that the answer is the one user has in mind | 34 | * Returns the approximate probability that the answer is the one user has in mind |
35 | * (as a double). | 35 | * (as a double).<br> |
36 | * The value ranges between 0 and 1. | ||
36 | * | 37 | * |
37 | * @return probability that this is the right answer. | 38 | * @return probability that this is the right answer. |
38 | */ | 39 | */ |
@@ -57,4 +58,15 @@ public interface Guess extends Identifiable, Comparable<Guess> { | |||
57 | @Nullable | 58 | @Nullable |
58 | URL getImage(); | 59 | URL getImage(); |
59 | 60 | ||
61 | /** | ||
62 | * <b>Important:</b> Akinator for some reason flags certain perfectly SFW guesses as | ||
63 | * explicit. Akiwrapper's code for this reason doesn't rely on this parameter - | ||
64 | * {@link Akiwrapper#suggestGuess} will return guesses marked as explicit even when | ||
65 | * profanity filtering is on. Using this value to filter content is optional but not | ||
66 | * advised. | ||
67 | * | ||
68 | * @return whether or not the guess is explicit (often incorrect). | ||
69 | */ | ||
70 | boolean isExplicit(); | ||
71 | |||
60 | } | 72 | } |
diff --git a/src/main/java/com/github/markozajc/akiwrapper/core/entities/Identifiable.java b/src/main/java/com/github/markozajc/akiwrapper/core/entities/Identifiable.java index 8e9b9fd..fb0105f 100644 --- a/src/main/java/com/github/markozajc/akiwrapper/core/entities/Identifiable.java +++ b/src/main/java/com/github/markozajc/akiwrapper/core/entities/Identifiable.java | |||
@@ -1,6 +1,6 @@ | |||
1 | package com.github.markozajc.akiwrapper.core.entities; | 1 | package com.github.markozajc.akiwrapper.core.entities; |
2 | 2 | ||
3 | import javax.annotation.*; | 3 | import javax.annotation.Nonnull; |
4 | 4 | ||
5 | /** | 5 | /** |
6 | * A representation of an object with a numeric identifier. Some objects in the API | 6 | * A representation of an object with a numeric identifier. Some objects in the API |
@@ -21,7 +21,6 @@ public interface Identifiable { | |||
21 | * | 21 | * |
22 | * @see #getId() | 22 | * @see #getId() |
23 | */ | 23 | */ |
24 | @Nonnegative | ||
25 | default long getIdLong() { | 24 | default long getIdLong() { |
26 | return Long.parseLong(getId()); | 25 | return Long.parseLong(getId()); |
27 | } | 26 | } |
diff --git a/src/main/java/com/github/markozajc/akiwrapper/core/entities/Question.java b/src/main/java/com/github/markozajc/akiwrapper/core/entities/Question.java index 2095d34..5ae4111 100644 --- a/src/main/java/com/github/markozajc/akiwrapper/core/entities/Question.java +++ b/src/main/java/com/github/markozajc/akiwrapper/core/entities/Question.java | |||
@@ -1,6 +1,6 @@ | |||
1 | package com.github.markozajc.akiwrapper.core.entities; | 1 | package com.github.markozajc.akiwrapper.core.entities; |
2 | 2 | ||
3 | import javax.annotation.*; | 3 | import javax.annotation.Nonnull; |
4 | 4 | ||
5 | import com.github.markozajc.akiwrapper.Akiwrapper.Answer; | 5 | import com.github.markozajc.akiwrapper.Akiwrapper.Answer; |
6 | import com.github.markozajc.akiwrapper.AkiwrapperBuilder; | 6 | import com.github.markozajc.akiwrapper.AkiwrapperBuilder; |
@@ -15,13 +15,12 @@ import com.github.markozajc.akiwrapper.AkiwrapperBuilder; | |||
15 | public interface Question extends Identifiable { | 15 | public interface Question extends Identifiable { |
16 | 16 | ||
17 | /** | 17 | /** |
18 | * Current completion percentage (as a double). Higher means that Akinator is closer | 18 | * Current completion percentage (as a double). Higher means that Akinator believes |
19 | * to the correct answer (or the game is close to the end?). Not sure if that's the | 19 | * to be closer to the correct answer.<br> |
20 | * case, but I believe this can go down as well. | 20 | * The value ranges between 0 and 100. |
21 | * | 21 | * |
22 | * @return completion percentage. | 22 | * @return completion percentage. |
23 | */ | 23 | */ |
24 | @Nonnegative | ||
25 | double getProgression(); | 24 | double getProgression(); |
26 | 25 | ||
27 | /** | 26 | /** |
@@ -30,37 +29,20 @@ public interface Question extends Identifiable { | |||
30 | * | 29 | * |
31 | * @return current step. | 30 | * @return current step. |
32 | */ | 31 | */ |
33 | @Nonnegative | ||
34 | int getStep(); | 32 | int getStep(); |
35 | 33 | ||
36 | /** | 34 | /** |
37 | * Returns the gained accuracy from the last question (as a double). I'm not exactly | 35 | * Returns the gained accuracy from the last question (as a double). I'm not exactly |
38 | * sure what this does, but I'm pretty sure that it's meant to describe how well | 36 | * sure what this does. |
39 | * Akinator can pinpoint the answer after a question was with the answered question. | ||
40 | * | 37 | * |
41 | * @return accuracy gain. | 38 | * @return infogain. |
42 | * | ||
43 | * @deprecated Use {@link #getInfogain()} instead | ||
44 | */ | ||
45 | @Nonnegative | ||
46 | @Deprecated(since = "1.5.2", forRemoval = true) | ||
47 | default double getGain() { | ||
48 | return getInfogain(); | ||
49 | } | ||
50 | |||
51 | /** | ||
52 | * Returns the gained accuracy from the last question (as a double). I'm not exactly | ||
53 | * sure what this does, but I'm pretty sure that it's meant to describe how well | ||
54 | * Akinator can pinpoint the answer after a question was with the answered question. | ||
55 | * | ||
56 | * @return accuracy gain. | ||
57 | */ | 39 | */ |
58 | @Nonnegative | ||
59 | double getInfogain(); | 40 | double getInfogain(); |
60 | 41 | ||
61 | /** | 42 | /** |
62 | * Returns the actual question that the user must answer. This is provided in the | 43 | * Returns the question content that should be displayed to the user. This localized |
63 | * language that was specified using the {@link AkiwrapperBuilder}. | 44 | * to the language specified in |
45 | * {@link AkiwrapperBuilder#setLanguage(Server.Language)}. | ||
64 | * | 46 | * |
65 | * @return question. | 47 | * @return question. |
66 | */ | 48 | */ |
diff --git a/src/main/java/com/github/markozajc/akiwrapper/core/entities/Server.java b/src/main/java/com/github/markozajc/akiwrapper/core/entities/Server.java index 7c1f104..3005590 100644 --- a/src/main/java/com/github/markozajc/akiwrapper/core/entities/Server.java +++ b/src/main/java/com/github/markozajc/akiwrapper/core/entities/Server.java | |||
@@ -2,23 +2,21 @@ package com.github.markozajc.akiwrapper.core.entities; | |||
2 | 2 | ||
3 | import javax.annotation.*; | 3 | import javax.annotation.*; |
4 | 4 | ||
5 | import com.github.markozajc.akiwrapper.core.Route; | ||
6 | import com.github.markozajc.akiwrapper.core.exceptions.ServerNotFoundException; | ||
7 | import com.github.markozajc.akiwrapper.core.utils.Servers; | 5 | import com.github.markozajc.akiwrapper.core.utils.Servers; |
8 | 6 | ||
9 | import kong.unirest.UnirestInstance; | 7 | import kong.unirest.UnirestInstance; |
10 | 8 | ||
11 | /** | 9 | /** |
12 | * A representation of an API server. All requests (except for | 10 | * A representation of an API server. Each server has a predefined {@link Language} |
13 | * {@link Route#NEW_SESSION} are passed to an such server. Each server has a | 11 | * and {@link GuessType}. |
14 | * predefined {@link Language} and {@link GuessType}. | ||
15 | * | 12 | * |
16 | * @author Marko Zajc | 13 | * @author Marko Zajc |
17 | */ | 14 | */ |
18 | public interface Server { | 15 | public interface Server { |
19 | 16 | ||
20 | /** | 17 | /** |
21 | * A language specific to a {@link Server}. | 18 | * A language specific to a {@link Server}. The server will return localized |
19 | * {@link Question}s and {@link Guess}es depending on its language. | ||
22 | * | 20 | * |
23 | * @author Marko Zajc | 21 | * @author Marko Zajc |
24 | */ | 22 | */ |
@@ -62,15 +60,10 @@ public interface Server { | |||
62 | } | 60 | } |
63 | 61 | ||
64 | /** | 62 | /** |
65 | * Server's guess type (referred to as the "subject" in the API). Decides what kind | 63 | * Represents the server's guess type (also referred to as the subject or theme). |
66 | * of things server's guesses will represent. While the name might suggest that this | 64 | * This decides what kind of things the {@link Server}'s {@link Guess}es will |
67 | * affects only {@link Guess}es, it will also inevitably also impact | 65 | * represent. While the name might imply that this affects only guess content, it |
68 | * {@link Question}s (it wouldn't make sense to ask the "Is it still alive" question | 66 | * also affects {@link Question}s.<br> |
69 | * for a place). <br> | ||
70 | * <b>Caution!</b> Not all {@link Language}s support all {@link GuessType}s. The | ||
71 | * standard ones seem to be {@link #ANIMAL}, {@link #CHARACTER}, and {@link #OBJECT}, | ||
72 | * but you might still face {@link ServerNotFoundException}s using them or other | ||
73 | * ones. | ||
74 | * | 67 | * |
75 | * @author Marko Zajc | 68 | * @author Marko Zajc |
76 | */ | 69 | */ |
@@ -104,7 +97,7 @@ public interface Server { | |||
104 | } | 97 | } |
105 | 98 | ||
106 | /** | 99 | /** |
107 | * Server's host name. As the people behind Akinator tend to mix up their servers and | 100 | * Server's base URL. As the people behind Akinator tend to mix up their servers and |
108 | * the API in general, this should only fetch values from the server-listing endpoint | 101 | * the API in general, this should only fetch values from the server-listing endpoint |
109 | * (which is done in {@link Servers#getServers(UnirestInstance)}. The host is a valid | 102 | * (which is done in {@link Servers#getServers(UnirestInstance)}. The host is a valid |
110 | * URL, complete with the path to the endpoint.<br> | 103 | * URL, complete with the path to the endpoint.<br> |
@@ -117,7 +110,7 @@ public interface Server { | |||
117 | 110 | ||
118 | /** | 111 | /** |
119 | * Returns this {@link Server}'s {@link Language}. The server will return localized | 112 | * Returns this {@link Server}'s {@link Language}. The server will return localized |
120 | * {@link Question}s and {@link Guess}es depending on its {@link Language}. | 113 | * {@link Question}s and {@link Guess}es depending on its language. |
121 | * | 114 | * |
122 | * @return server's language. | 115 | * @return server's language. |
123 | */ | 116 | */ |
diff --git a/src/main/java/com/github/markozajc/akiwrapper/core/entities/ServerList.java b/src/main/java/com/github/markozajc/akiwrapper/core/entities/ServerList.java deleted file mode 100644 index c3a4552..0000000 --- a/src/main/java/com/github/markozajc/akiwrapper/core/entities/ServerList.java +++ /dev/null | |||
@@ -1,72 +0,0 @@ | |||
1 | package com.github.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.github.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/github/markozajc/akiwrapper/core/entities/Status.java b/src/main/java/com/github/markozajc/akiwrapper/core/entities/Status.java index 650fedc..c6403e3 100644 --- a/src/main/java/com/github/markozajc/akiwrapper/core/entities/Status.java +++ b/src/main/java/com/github/markozajc/akiwrapper/core/entities/Status.java | |||
@@ -4,6 +4,8 @@ import java.io.Serializable; | |||
4 | 4 | ||
5 | import javax.annotation.*; | 5 | import javax.annotation.*; |
6 | 6 | ||
7 | import com.github.markozajc.akiwrapper.core.exceptions.ServerStatusException; | ||
8 | |||
7 | /** | 9 | /** |
8 | * An interface used to represent API call's completion status. | 10 | * An interface used to represent API call's completion status. |
9 | * | 11 | * |
@@ -32,14 +34,6 @@ public interface Status extends Serializable { | |||
32 | ERROR("KO"), | 34 | ERROR("KO"), |
33 | 35 | ||
34 | /** | 36 | /** |
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 | 37 | * Unknown status (should not ever occur under normal circumstances), indicates that |
44 | * the status level doesn't match any of the known ones. | 38 | * the status level doesn't match any of the known ones. |
45 | */ | 39 | */ |
@@ -60,6 +54,7 @@ public interface Status extends Serializable { | |||
60 | } | 54 | } |
61 | 55 | ||
62 | @Nonnull | 56 | @Nonnull |
57 | @SuppressWarnings("javadoc") // internal impl | ||
63 | public static Level fromString(@Nonnull String completion) { | 58 | public static Level fromString(@Nonnull String completion) { |
64 | for (Level iteratedLevel : Level.values()) | 59 | for (Level iteratedLevel : Level.values()) |
65 | if (completion.toUpperCase().startsWith(iteratedLevel.toString())) | 60 | if (completion.toUpperCase().startsWith(iteratedLevel.toString())) |
@@ -71,6 +66,45 @@ public interface Status extends Serializable { | |||
71 | } | 66 | } |
72 | 67 | ||
73 | /** | 68 | /** |
69 | * A less cryptic form of the status message, which helps you distinguish between a | ||
70 | * usage error (a problem with your code), a library error (a problem with Akiwrapper | ||
71 | * that should be reported), a server error (a problem with Akinator's servers that | ||
72 | * only they can fix), or an unproblematic status. | ||
73 | */ | ||
74 | public enum Reason { | ||
75 | |||
76 | /** | ||
77 | * <b>Note:</b> This {@link Reason} should generally not find its way into a | ||
78 | * {@link ServerStatusException}, please open an issue if it ever does.<br> | ||
79 | * <br> | ||
80 | * The status is non-erroneous. | ||
81 | */ | ||
82 | OK, | ||
83 | /** | ||
84 | * <b>Note:</b> This {@link Reason} should generally not find its way into a | ||
85 | * {@link ServerStatusException}, please open an issue if it ever does.<br> | ||
86 | * <br> | ||
87 | * The status is non-erroneous and the questions have been exhausted. | ||
88 | */ | ||
89 | QUESTIONS_EXHAUSTED, | ||
90 | /** | ||
91 | * The status is erroneous and likely caused by a bug in the library. Please open an | ||
92 | * issue if this occurs. | ||
93 | */ | ||
94 | LIBRARY_FAILURE, | ||
95 | /** | ||
96 | * The status is erroneous and likely caused by a problem with Akinator's servers. | ||
97 | * Please only open an issue if this occurs consistently for a period of time. | ||
98 | */ | ||
99 | SERVER_FAILURE, | ||
100 | /** | ||
101 | * The reason is unknown. Refer to the status message and level for more details. | ||
102 | */ | ||
103 | UNKNOWN | ||
104 | |||
105 | } | ||
106 | |||
107 | /** | ||
74 | * Returns the level of this status. Status level indicates severity of the status. | 108 | * Returns the level of this status. Status level indicates severity of the status. |
75 | * | 109 | * |
76 | * @return status level | 110 | * @return status level |
@@ -78,13 +112,27 @@ public interface Status extends Serializable { | |||
78 | Level getLevel(); | 112 | Level getLevel(); |
79 | 113 | ||
80 | /** | 114 | /** |
81 | * Returns the status reason or {@code null} if it was not specified. Note that the | 115 | * Returns the status message or {@code null} if it was not specified. Note that the |
82 | * status reason is usually pretty cryptic and won't mean much to regular users or | 116 | * status message is usually pretty cryptic and won't mean much to regular users or |
83 | * anyone not experienced with the Akinator API. | 117 | * anyone not experienced with the Akinator API. If you need something something more |
118 | * tangible, use {@link #getReason()}. | ||
84 | * | 119 | * |
85 | * @return status reason | 120 | * @return status message |
86 | */ | 121 | */ |
87 | @Nullable | 122 | @Nullable |
88 | String getReason(); | 123 | String getMessage(); |
124 | |||
125 | /** | ||
126 | * Returns the status reason, which is picked from a list of predefined values or | ||
127 | * {@link Reason#UNKNOWN} if it's meaning/significance are unknown. This generally | ||
128 | * helps distinguish between a usage error (a problem with your code), a library | ||
129 | * error (a problem with Akiwrapper that should be reported), a server error (a | ||
130 | * problem with Akinator's servers that only they can fix), or an unproblematic | ||
131 | * status. | ||
132 | * | ||
133 | * @return status {@link Reason} | ||
134 | */ | ||
135 | @Nonnull | ||
136 | Reason getReason(); | ||
89 | 137 | ||
90 | } | 138 | } |
diff --git a/src/main/java/com/github/markozajc/akiwrapper/core/entities/impl/immutable/GuessImpl.java b/src/main/java/com/github/markozajc/akiwrapper/core/entities/impl/GuessImpl.java index f2afa77..5e2080c 100644 --- a/src/main/java/com/github/markozajc/akiwrapper/core/entities/impl/immutable/GuessImpl.java +++ b/src/main/java/com/github/markozajc/akiwrapper/core/entities/impl/GuessImpl.java | |||
@@ -1,6 +1,6 @@ | |||
1 | package com.github.markozajc.akiwrapper.core.entities.impl.immutable; | 1 | package com.github.markozajc.akiwrapper.core.entities.impl; |
2 | 2 | ||
3 | import static com.github.markozajc.akiwrapper.core.utils.JSONUtils.getDouble; | 3 | import static com.github.markozajc.akiwrapper.core.utils.JSONUtils.*; |
4 | import static java.lang.Double.compare; | 4 | import static java.lang.Double.compare; |
5 | 5 | ||
6 | import java.net.*; | 6 | import java.net.*; |
@@ -11,27 +11,30 @@ import org.json.JSONObject; | |||
11 | 11 | ||
12 | import com.github.markozajc.akiwrapper.core.entities.Guess; | 12 | import com.github.markozajc.akiwrapper.core.entities.Guess; |
13 | 13 | ||
14 | @SuppressWarnings("javadoc") // internal impl | ||
14 | public class GuessImpl implements Guess { | 15 | public class GuessImpl implements Guess { |
15 | 16 | ||
16 | @Nonnull private final String id; | 17 | @Nonnull private final String id; |
17 | @Nonnull private final String name; | 18 | @Nonnull private final String name; |
18 | @Nullable private final String description; | 19 | @Nullable private final String description; |
19 | @Nullable private final URL image; | 20 | @Nullable private final URL image; |
20 | @Nonnegative private final double probability; | 21 | private final double probability; |
22 | private final boolean explicit; | ||
21 | 23 | ||
22 | public GuessImpl(@Nonnull String id, @Nonnull String name, @Nullable String description, @Nullable URL image, | 24 | GuessImpl(@Nonnull String id, @Nonnull String name, @Nullable String description, @Nullable URL image, |
23 | @Nonnegative double probability) { | 25 | @Nonnegative double probability, boolean explicit) { |
24 | this.id = id; | 26 | this.id = id; |
25 | this.name = name; | 27 | this.name = name; |
26 | this.description = description; | 28 | this.description = description; |
27 | this.image = image; | 29 | this.image = image; |
28 | this.probability = probability; | 30 | this.probability = probability; |
31 | this.explicit = explicit; | ||
29 | } | 32 | } |
30 | 33 | ||
31 | @SuppressWarnings("null") | 34 | @SuppressWarnings("null") |
32 | public static GuessImpl from(@Nonnull JSONObject json) { | 35 | public static GuessImpl fromJson(@Nonnull JSONObject json) { |
33 | return new GuessImpl(json.getString("id"), json.getString("name"), getDescription(json), getImage(json), | 36 | return new GuessImpl(json.getString("id"), json.getString("name"), getDescription(json), getImage(json), |
34 | getDouble(json, "proba").orElseThrow()); | 37 | getDouble(json, "proba").orElseThrow(), getInteger(json, "corrupt").orElseThrow() == 1); |
35 | } | 38 | } |
36 | 39 | ||
37 | @Nullable | 40 | @Nullable |
@@ -81,4 +84,9 @@ public class GuessImpl implements Guess { | |||
81 | return compare(o.getProbability(), this.probability); | 84 | return compare(o.getProbability(), this.probability); |
82 | } | 85 | } |
83 | 86 | ||
87 | @Override | ||
88 | public boolean isExplicit() { | ||
89 | return this.explicit; | ||
90 | } | ||
91 | |||
84 | } | 92 | } |
diff --git a/src/main/java/com/github/markozajc/akiwrapper/core/entities/impl/QuestionImpl.java b/src/main/java/com/github/markozajc/akiwrapper/core/entities/impl/QuestionImpl.java new file mode 100644 index 0000000..f96ed0c --- /dev/null +++ b/src/main/java/com/github/markozajc/akiwrapper/core/entities/impl/QuestionImpl.java | |||
@@ -0,0 +1,61 @@ | |||
1 | package com.github.markozajc.akiwrapper.core.entities.impl; | ||
2 | |||
3 | import static com.github.markozajc.akiwrapper.core.utils.JSONUtils.*; | ||
4 | |||
5 | import javax.annotation.*; | ||
6 | |||
7 | import org.json.JSONObject; | ||
8 | |||
9 | import com.github.markozajc.akiwrapper.core.entities.Question; | ||
10 | |||
11 | @SuppressWarnings("javadoc") // internal impl | ||
12 | public class QuestionImpl implements Question { | ||
13 | |||
14 | @Nonnull private final String id; | ||
15 | @Nonnull private final String question; | ||
16 | private final int step; | ||
17 | private final double gain; | ||
18 | private final double progression; | ||
19 | |||
20 | private QuestionImpl(@Nonnull String id, @Nonnull String question, @Nonnegative int step, @Nonnegative double gain, | ||
21 | @Nonnegative double progression) { | ||
22 | this.id = id; | ||
23 | this.question = question; | ||
24 | this.step = step; | ||
25 | this.gain = gain; | ||
26 | this.progression = progression; | ||
27 | } | ||
28 | |||
29 | @SuppressWarnings("null") | ||
30 | public static QuestionImpl fromJson(@Nonnull JSONObject json) { | ||
31 | return new QuestionImpl(json.getString("questionid"), json.getString("question"), | ||
32 | getInteger(json, "step").orElseThrow(), getDouble(json, "infogain").orElseThrow(), | ||
33 | getDouble(json, "progression").orElseThrow()); | ||
34 | } | ||
35 | |||
36 | @Override | ||
37 | public double getProgression() { | ||
38 | return this.progression; | ||
39 | } | ||
40 | |||
41 | @Override | ||
42 | public int getStep() { | ||
43 | return this.step; | ||
44 | } | ||
45 | |||
46 | @Override | ||
47 | public double getInfogain() { | ||
48 | return this.gain; | ||
49 | } | ||
50 | |||
51 | @Override | ||
52 | public String getQuestion() { | ||
53 | return this.question; | ||
54 | } | ||
55 | |||
56 | @Override | ||
57 | public String getId() { | ||
58 | return this.id; | ||
59 | } | ||
60 | |||
61 | } | ||
diff --git a/src/main/java/com/github/markozajc/akiwrapper/core/entities/impl/immutable/ServerImpl.java b/src/main/java/com/github/markozajc/akiwrapper/core/entities/impl/ServerImpl.java index 7af6db8..8d388f0 100644 --- a/src/main/java/com/github/markozajc/akiwrapper/core/entities/impl/immutable/ServerImpl.java +++ b/src/main/java/com/github/markozajc/akiwrapper/core/entities/impl/ServerImpl.java | |||
@@ -1,5 +1,8 @@ | |||
1 | package com.github.markozajc.akiwrapper.core.entities.impl.immutable; | 1 | package com.github.markozajc.akiwrapper.core.entities.impl; |
2 | 2 | ||
3 | import static java.nio.charset.StandardCharsets.UTF_8; | ||
4 | |||
5 | import java.net.URLEncoder; | ||
3 | import java.util.List; | 6 | import java.util.List; |
4 | import java.util.stream.Collectors; | 7 | import java.util.stream.Collectors; |
5 | 8 | ||
@@ -8,6 +11,7 @@ import javax.annotation.Nonnull; | |||
8 | import com.github.markozajc.akiwrapper.core.entities.Server; | 11 | import com.github.markozajc.akiwrapper.core.entities.Server; |
9 | import com.jcabi.xml.XML; | 12 | import com.jcabi.xml.XML; |
10 | 13 | ||
14 | @SuppressWarnings("javadoc") // internal impl | ||
11 | public class ServerImpl implements Server { | 15 | public class ServerImpl implements Server { |
12 | 16 | ||
13 | private static final String LANGUAGE_ID_XPATH = "LANGUAGE/LANG_ID/text()"; // NOSONAR not a URL | 17 | private static final String LANGUAGE_ID_XPATH = "LANGUAGE/LANG_ID/text()"; // NOSONAR not a URL |
@@ -57,4 +61,9 @@ public class ServerImpl implements Server { | |||
57 | return this.url; | 61 | return this.url; |
58 | } | 62 | } |
59 | 63 | ||
64 | @Nonnull | ||
65 | public String asUrlApiWs() { | ||
66 | return "urlApiWs=" + URLEncoder.encode(this.url, UTF_8); | ||
67 | } | ||
68 | |||
60 | } | 69 | } |
diff --git a/src/main/java/com/github/markozajc/akiwrapper/core/entities/impl/StatusImpl.java b/src/main/java/com/github/markozajc/akiwrapper/core/entities/impl/StatusImpl.java new file mode 100644 index 0000000..15fe5a0 --- /dev/null +++ b/src/main/java/com/github/markozajc/akiwrapper/core/entities/impl/StatusImpl.java | |||
@@ -0,0 +1,97 @@ | |||
1 | package com.github.markozajc.akiwrapper.core.entities.impl; | ||
2 | |||
3 | import static com.github.markozajc.akiwrapper.core.entities.Status.Reason.UNKNOWN; | ||
4 | |||
5 | import javax.annotation.*; | ||
6 | |||
7 | import org.json.JSONObject; | ||
8 | |||
9 | import com.github.markozajc.akiwrapper.core.entities.Status; | ||
10 | |||
11 | @SuppressWarnings("javadoc") // internal impl | ||
12 | public class StatusImpl implements Status { | ||
13 | |||
14 | private static final String DIVIDER = " - "; | ||
15 | |||
16 | @Nonnull private final Level level; | ||
17 | @Nullable private final String message; | ||
18 | @Nonnull private final Reason reason; | ||
19 | |||
20 | public static StatusImpl fromCompletion(@Nonnull String completion) { | ||
21 | var level = Level.fromString(completion); | ||
22 | var message = determineMessage(completion); | ||
23 | var reason = resolveReason(level, message); | ||
24 | return new StatusImpl(level, message, reason); | ||
25 | } | ||
26 | |||
27 | @SuppressWarnings("null") | ||
28 | public static StatusImpl fromJson(@Nonnull JSONObject json) { | ||
29 | return fromCompletion(json.getString("completion")); | ||
30 | } | ||
31 | |||
32 | private StatusImpl(@Nonnull Level level, @Nullable String message, @Nonnull Reason reason) { | ||
33 | this.level = level; | ||
34 | this.message = message; | ||
35 | this.reason = reason; | ||
36 | } | ||
37 | |||
38 | @Nullable | ||
39 | private static String determineMessage(@Nonnull String completion) { | ||
40 | int reasonSplitIndex = completion.indexOf(DIVIDER); | ||
41 | if (reasonSplitIndex != -1) | ||
42 | return completion.substring(reasonSplitIndex + DIVIDER.length()); | ||
43 | return null; | ||
44 | } | ||
45 | |||
46 | @Override | ||
47 | public Level getLevel() { | ||
48 | return this.level; | ||
49 | } | ||
50 | |||
51 | @Override | ||
52 | public String getMessage() { | ||
53 | return this.message; | ||
54 | } | ||
55 | |||
56 | @Override | ||
57 | public Reason getReason() { | ||
58 | return this.reason; | ||
59 | } | ||
60 | |||
61 | @Nonnull | ||
62 | public static Reason resolveReason(@Nonnull Level level, @Nullable String message) { | ||
63 | if (level == Level.OK && message == null) { | ||
64 | return Reason.OK; | ||
65 | } else if (level == Level.WARNING && message != null && message.equals("NO QUESTION")) { | ||
66 | return Reason.QUESTIONS_EXHAUSTED; | ||
67 | } else if (level == Level.ERROR && message != null) { | ||
68 | if (message.equals("TECHNICAL ERROR")) | ||
69 | return Reason.SERVER_FAILURE; | ||
70 | else if (message.equals("MISSING KEY") || message.equals("ELEM LIST IS EMPTY") | ||
71 | || message.equals("MISSING PARAMETERS") || message.equals("UNAUTHORIZED")) | ||
72 | return Reason.LIBRARY_FAILURE; | ||
73 | } | ||
74 | |||
75 | return UNKNOWN; | ||
76 | } | ||
77 | |||
78 | @Override | ||
79 | public String toString() { | ||
80 | var sb = new StringBuilder(); | ||
81 | sb.append(this.level); | ||
82 | |||
83 | if (this.message != null) { | ||
84 | sb.append(" - "); | ||
85 | sb.append(this.message); | ||
86 | } | ||
87 | |||
88 | if (this.reason != UNKNOWN) { | ||
89 | sb.append(" ("); | ||
90 | sb.append(this.reason); | ||
91 | sb.append(')'); | ||
92 | } | ||
93 | |||
94 | return sb.toString(); | ||
95 | } | ||
96 | |||
97 | } | ||
diff --git a/src/main/java/com/github/markozajc/akiwrapper/core/entities/impl/immutable/QuestionImpl.java b/src/main/java/com/github/markozajc/akiwrapper/core/entities/impl/immutable/QuestionImpl.java deleted file mode 100644 index c5b77cf..0000000 --- a/src/main/java/com/github/markozajc/akiwrapper/core/entities/impl/immutable/QuestionImpl.java +++ /dev/null | |||
@@ -1,71 +0,0 @@ | |||
1 | package com.github.markozajc.akiwrapper.core.entities.impl.immutable; | ||
2 | |||
3 | import static com.github.markozajc.akiwrapper.core.entities.Status.Level.WARNING; | ||
4 | import static com.github.markozajc.akiwrapper.core.utils.JSONUtils.*; | ||
5 | |||
6 | import javax.annotation.*; | ||
7 | |||
8 | import org.json.JSONObject; | ||
9 | |||
10 | import com.github.markozajc.akiwrapper.core.entities.*; | ||
11 | import com.github.markozajc.akiwrapper.core.exceptions.MissingQuestionException; | ||
12 | |||
13 | public class QuestionImpl implements Question { | ||
14 | |||
15 | private static final String REASON_OUT_OF_QUESTIONS = "no question"; | ||
16 | |||
17 | @Nonnull private final String id; | ||
18 | @Nonnull private final String question; | ||
19 | @Nonnegative private final int step; | ||
20 | @Nonnegative private final double gain; | ||
21 | @Nonnegative private final double progression; | ||
22 | |||
23 | public QuestionImpl(@Nonnull String id, @Nonnull String question, @Nonnegative int step, @Nonnegative double gain, | ||
24 | @Nonnegative double progression, @Nonnull Status status) { | ||
25 | checkMissingQuestion(status); | ||
26 | |||
27 | this.id = id; | ||
28 | this.question = question; | ||
29 | this.step = step; | ||
30 | this.gain = gain; | ||
31 | this.progression = progression; | ||
32 | } | ||
33 | |||
34 | @SuppressWarnings("null") | ||
35 | public static QuestionImpl from(@Nonnull JSONObject json, @Nonnull Status status) { | ||
36 | return new QuestionImpl(json.getString("questionid"), json.getString("question"), | ||
37 | getInteger(json, "step").orElseThrow(), getDouble(json, "infogain").orElseThrow(), | ||
38 | getDouble(json, "progression").orElseThrow(), status); | ||
39 | } | ||
40 | |||
41 | private static void checkMissingQuestion(@Nonnull Status status) { | ||
42 | if (status.getLevel() == WARNING && REASON_OUT_OF_QUESTIONS.equalsIgnoreCase(status.getReason())) | ||
43 | throw new MissingQuestionException(); | ||
44 | } | ||
45 | |||
46 | @Override | ||
47 | public double getProgression() { | ||
48 | return this.progression; | ||
49 | } | ||
50 | |||
51 | @Override | ||
52 | public int getStep() { | ||
53 | return this.step; | ||
54 | } | ||
55 | |||
56 | @Override | ||
57 | public double getInfogain() { | ||
58 | return this.gain; | ||
59 | } | ||
60 | |||
61 | @Override | ||
62 | public String getQuestion() { | ||
63 | return this.question; | ||
64 | } | ||
65 | |||
66 | @Override | ||
67 | public String getId() { | ||
68 | return this.id; | ||
69 | } | ||
70 | |||
71 | } | ||
diff --git a/src/main/java/com/github/markozajc/akiwrapper/core/entities/impl/immutable/ServerListImpl.java b/src/main/java/com/github/markozajc/akiwrapper/core/entities/impl/immutable/ServerListImpl.java deleted file mode 100644 index 2a3114c..0000000 --- a/src/main/java/com/github/markozajc/akiwrapper/core/entities/impl/immutable/ServerListImpl.java +++ /dev/null | |||
@@ -1,85 +0,0 @@ | |||
1 | package com.github.markozajc.akiwrapper.core.entities.impl.immutable; | ||
2 | |||
3 | import java.util.*; | ||
4 | import java.util.concurrent.ConcurrentLinkedQueue; | ||
5 | import java.util.stream.*; | ||
6 | |||
7 | import javax.annotation.Nonnull; | ||
8 | |||
9 | import com.github.markozajc.akiwrapper.core.entities.*; | ||
10 | |||
11 | public class ServerListImpl implements ServerList { | ||
12 | |||
13 | @Nonnull private Server currentServer; | ||
14 | @Nonnull private final Queue<Server> candidateServers; | ||
15 | |||
16 | @SuppressWarnings("null") | ||
17 | public ServerListImpl(@Nonnull Server first, @Nonnull Server... candidates) { | ||
18 | this(first, Arrays.asList(candidates)); | ||
19 | } | ||
20 | |||
21 | public ServerListImpl(@Nonnull Server first, @Nonnull Collection<Server> candidates) { | ||
22 | this.candidateServers = unwrapServersIntoQueue(candidates); | ||
23 | this.currentServer = first; | ||
24 | } | ||
25 | |||
26 | @SuppressWarnings("null") | ||
27 | public ServerListImpl(@Nonnull Collection<Server> servers) { | ||
28 | if (servers.isEmpty()) | ||
29 | throw new IllegalArgumentException("The collection of servers may not be empty"); | ||
30 | |||
31 | ConcurrentLinkedQueue<Server> queue = unwrapServersIntoQueue(servers); | ||
32 | this.candidateServers = queue; | ||
33 | this.currentServer = this.candidateServers.remove(); | ||
34 | } | ||
35 | |||
36 | @SuppressWarnings("null") | ||
37 | @Nonnull | ||
38 | private static ConcurrentLinkedQueue<Server> unwrapServersIntoQueue(@Nonnull Collection<Server> servers) { | ||
39 | return servers.stream().flatMap(s -> { | ||
40 | if (s instanceof ServerList) | ||
41 | return ((ServerList) s).getServers().stream(); | ||
42 | else | ||
43 | return Stream.of(s); | ||
44 | }).collect(Collectors.toCollection(ConcurrentLinkedQueue<Server>::new)); | ||
45 | } | ||
46 | |||
47 | @Override | ||
48 | public String getUrl() { | ||
49 | return this.currentServer.getUrl(); | ||
50 | } | ||
51 | |||
52 | @Override | ||
53 | public Language getLanguage() { | ||
54 | return this.currentServer.getLanguage(); | ||
55 | } | ||
56 | |||
57 | @Override | ||
58 | public GuessType getGuessType() { | ||
59 | return this.currentServer.getGuessType(); | ||
60 | } | ||
61 | |||
62 | @SuppressWarnings("null") | ||
63 | @Override | ||
64 | public boolean next() { | ||
65 | if (!hasNext()) | ||
66 | return false; | ||
67 | |||
68 | this.currentServer = this.candidateServers.remove(); | ||
69 | return true; | ||
70 | } | ||
71 | |||
72 | @Override | ||
73 | public List<Server> getServers() { | ||
74 | List<Server> result = new ArrayList<>(getRemainingSize() + 1); | ||
75 | result.add(this.currentServer); | ||
76 | result.addAll(this.candidateServers); | ||
77 | return result; | ||
78 | } | ||
79 | |||
80 | @Override | ||
81 | public int getRemainingSize() { | ||
82 | return this.candidateServers.size(); | ||
83 | } | ||
84 | |||
85 | } | ||
diff --git a/src/main/java/com/github/markozajc/akiwrapper/core/entities/impl/immutable/StatusImpl.java b/src/main/java/com/github/markozajc/akiwrapper/core/entities/impl/immutable/StatusImpl.java deleted file mode 100644 index b3c2381..0000000 --- a/src/main/java/com/github/markozajc/akiwrapper/core/entities/impl/immutable/StatusImpl.java +++ /dev/null | |||
@@ -1,61 +0,0 @@ | |||
1 | package com.github.markozajc.akiwrapper.core.entities.impl.immutable; | ||
2 | |||
3 | import static java.lang.String.format; | ||
4 | |||
5 | import javax.annotation.*; | ||
6 | |||
7 | import org.json.JSONObject; | ||
8 | |||
9 | import com.github.markozajc.akiwrapper.core.entities.Status; | ||
10 | |||
11 | public class StatusImpl implements Status { | ||
12 | |||
13 | private static final String DIVIDER = " - "; | ||
14 | private static final String STATUS_FORMAT = "%s" + DIVIDER + "%s"; | ||
15 | |||
16 | public static final StatusImpl STATUS_OK = new StatusImpl(Level.OK, null); | ||
17 | |||
18 | @Nullable private final String reason; | ||
19 | @Nonnull private final Level level; | ||
20 | |||
21 | private StatusImpl(@Nonnull Level level, @Nullable String reason) { | ||
22 | this.level = level; | ||
23 | this.reason = reason; | ||
24 | } | ||
25 | |||
26 | public StatusImpl(@Nonnull String completion) { | ||
27 | this(Level.fromString(completion), determineReason(completion)); | ||
28 | } | ||
29 | |||
30 | @SuppressWarnings("null") | ||
31 | public StatusImpl(@Nonnull JSONObject json) { | ||
32 | this(json.getString("completion")); | ||
33 | } | ||
34 | |||
35 | @Nullable | ||
36 | private static String determineReason(@Nonnull String completion) { | ||
37 | int reasonSplitIndex = completion.indexOf(DIVIDER); | ||
38 | if (reasonSplitIndex != -1) | ||
39 | return completion.substring(reasonSplitIndex + DIVIDER.length()); | ||
40 | return null; | ||
41 | } | ||
42 | |||
43 | @Override | ||
44 | public String getReason() { | ||
45 | return this.reason; | ||
46 | } | ||
47 | |||
48 | @Override | ||
49 | public Level getLevel() { | ||
50 | return this.level; | ||
51 | } | ||
52 | |||
53 | @Override | ||
54 | public String toString() { | ||
55 | if (getReason() == null) | ||
56 | return getLevel().toString(); | ||
57 | else | ||
58 | return format(STATUS_FORMAT, getLevel().toString(), getReason()); | ||
59 | } | ||
60 | |||
61 | } | ||
diff --git a/src/main/java/com/github/markozajc/akiwrapper/core/exceptions/AkinatorException.java b/src/main/java/com/github/markozajc/akiwrapper/core/exceptions/AkinatorException.java new file mode 100644 index 0000000..152cb95 --- /dev/null +++ b/src/main/java/com/github/markozajc/akiwrapper/core/exceptions/AkinatorException.java | |||
@@ -0,0 +1,25 @@ | |||
1 | package com.github.markozajc.akiwrapper.core.exceptions; | ||
2 | |||
3 | /** | ||
4 | * The root exception class for exceptions in Akiwrapper. | ||
5 | * | ||
6 | * @author Marko Zajc | ||
7 | */ | ||
8 | public class AkinatorException extends RuntimeException { | ||
9 | |||
10 | @SuppressWarnings("javadoc") // internal | ||
11 | public AkinatorException() { | ||
12 | super(); | ||
13 | } | ||
14 | |||
15 | @SuppressWarnings("javadoc") // internal | ||
16 | public AkinatorException(String message) { | ||
17 | super(message); | ||
18 | } | ||
19 | |||
20 | @SuppressWarnings("javadoc") // internal | ||
21 | public AkinatorException(String message, Throwable cause) { | ||
22 | super(message, cause); | ||
23 | } | ||
24 | |||
25 | } | ||
diff --git a/src/main/java/com/github/markozajc/akiwrapper/core/exceptions/MissingQuestionException.java b/src/main/java/com/github/markozajc/akiwrapper/core/exceptions/MissingQuestionException.java deleted file mode 100644 index 89739c6..0000000 --- a/src/main/java/com/github/markozajc/akiwrapper/core/exceptions/MissingQuestionException.java +++ /dev/null | |||
@@ -1,15 +0,0 @@ | |||
1 | package com.github.markozajc.akiwrapper.core.exceptions; | ||
2 | |||
3 | /** | ||
4 | * An exception indicating that there is no question left to answer or fetch. | ||
5 | * | ||
6 | * @author Marko Zajc | ||
7 | */ | ||
8 | public class MissingQuestionException extends RuntimeException { | ||
9 | |||
10 | /** | ||
11 | * Constructs a new {@link MissingQuestionException} instance. | ||
12 | */ | ||
13 | public MissingQuestionException() {} | ||
14 | |||
15 | } | ||
diff --git a/src/main/java/com/github/markozajc/akiwrapper/core/exceptions/QuestionsExhaustedException.java b/src/main/java/com/github/markozajc/akiwrapper/core/exceptions/QuestionsExhaustedException.java new file mode 100644 index 0000000..25e834a --- /dev/null +++ b/src/main/java/com/github/markozajc/akiwrapper/core/exceptions/QuestionsExhaustedException.java | |||
@@ -0,0 +1,13 @@ | |||
1 | package com.github.markozajc.akiwrapper.core.exceptions; | ||
2 | |||
3 | /** | ||
4 | * An exception indicating that there is no question left to answer or fetch. | ||
5 | * | ||
6 | * @author Marko Zajc | ||
7 | */ | ||
8 | public class QuestionsExhaustedException extends AkinatorException { | ||
9 | |||
10 | @SuppressWarnings("javadoc") // internal | ||
11 | public QuestionsExhaustedException() {} | ||
12 | |||
13 | } | ||
diff --git a/src/main/java/com/github/markozajc/akiwrapper/core/exceptions/ServerNotFoundException.java b/src/main/java/com/github/markozajc/akiwrapper/core/exceptions/ServerNotFoundException.java index dbec147..f19bd93 100644 --- a/src/main/java/com/github/markozajc/akiwrapper/core/exceptions/ServerNotFoundException.java +++ b/src/main/java/com/github/markozajc/akiwrapper/core/exceptions/ServerNotFoundException.java | |||
@@ -1,5 +1,7 @@ | |||
1 | package com.github.markozajc.akiwrapper.core.exceptions; | 1 | package com.github.markozajc.akiwrapper.core.exceptions; |
2 | 2 | ||
3 | import javax.annotation.Nonnull; | ||
4 | |||
3 | import com.github.markozajc.akiwrapper.core.entities.Server; | 5 | import com.github.markozajc.akiwrapper.core.entities.Server; |
4 | import com.github.markozajc.akiwrapper.core.entities.Server.*; | 6 | import com.github.markozajc.akiwrapper.core.entities.Server.*; |
5 | 7 | ||
@@ -9,11 +11,11 @@ import com.github.markozajc.akiwrapper.core.entities.Server.*; | |||
9 | * | 11 | * |
10 | * @author Marko Zajc | 12 | * @author Marko Zajc |
11 | */ | 13 | */ |
12 | public class ServerNotFoundException extends Exception { | 14 | public class ServerNotFoundException extends AkinatorException { |
13 | 15 | ||
14 | /** | 16 | @SuppressWarnings("javadoc") // internal |
15 | * Constructs a new {@link ServerNotFoundException}. | 17 | public ServerNotFoundException(@Nonnull String message) { |
16 | */ | 18 | super(message); |
17 | public ServerNotFoundException() {} | 19 | } |
18 | 20 | ||
19 | } | 21 | } |
diff --git a/src/main/java/com/github/markozajc/akiwrapper/core/exceptions/ServerStatusException.java b/src/main/java/com/github/markozajc/akiwrapper/core/exceptions/ServerStatusException.java new file mode 100644 index 0000000..b21692e --- /dev/null +++ b/src/main/java/com/github/markozajc/akiwrapper/core/exceptions/ServerStatusException.java | |||
@@ -0,0 +1,29 @@ | |||
1 | package com.github.markozajc.akiwrapper.core.exceptions; | ||
2 | |||
3 | import com.github.markozajc.akiwrapper.core.entities.Status; | ||
4 | import com.github.markozajc.akiwrapper.core.entities.Status.Level; | ||
5 | |||
6 | /** | ||
7 | * An exception indicating that the server returned an error code | ||
8 | * ({@link Level#ERROR}). | ||
9 | * | ||
10 | * @author Marko Zajc | ||
11 | */ | ||
12 | public class ServerStatusException extends AkinatorException { | ||
13 | |||
14 | private final Status status; | ||
15 | |||
16 | @SuppressWarnings("javadoc") // internal | ||
17 | public ServerStatusException(Status status) { | ||
18 | super(status.toString()); | ||
19 | this.status = status; | ||
20 | } | ||
21 | |||
22 | /** | ||
23 | * @return the erroneous status returned by the server | ||
24 | */ | ||
25 | public Status getStatus() { | ||
26 | return this.status; | ||
27 | } | ||
28 | |||
29 | } | ||
diff --git a/src/main/java/com/github/markozajc/akiwrapper/core/exceptions/ServerUnavailableException.java b/src/main/java/com/github/markozajc/akiwrapper/core/exceptions/ServerUnavailableException.java deleted file mode 100644 index 61b503f..0000000 --- a/src/main/java/com/github/markozajc/akiwrapper/core/exceptions/ServerUnavailableException.java +++ /dev/null | |||
@@ -1,35 +0,0 @@ | |||
1 | package com.github.markozajc.akiwrapper.core.exceptions; | ||
2 | |||
3 | import javax.annotation.Nonnull; | ||
4 | |||
5 | import com.github.markozajc.akiwrapper.core.entities.*; | ||
6 | import com.github.markozajc.akiwrapper.core.entities.impl.immutable.StatusImpl; | ||
7 | |||
8 | /** | ||
9 | * An exception indicating that the currently used {@link Server} has gone offline. | ||
10 | * | ||
11 | * @author Marko Zajc | ||
12 | */ | ||
13 | public class ServerUnavailableException extends StatusException { | ||
14 | |||
15 | /** | ||
16 | * Constructs a new {@link ServerUnavailableException} from a {@link Status}. | ||
17 | * | ||
18 | * @param status | ||
19 | * erroneous status. | ||
20 | */ | ||
21 | public ServerUnavailableException(@Nonnull Status status) { | ||
22 | super(status); | ||
23 | } | ||
24 | |||
25 | /** | ||
26 | * Constructs a new {@link ServerUnavailableException} from a {@link Status} string. | ||
27 | * | ||
28 | * @param status | ||
29 | * erroneous status string. | ||
30 | */ | ||
31 | public ServerUnavailableException(@Nonnull String status) { | ||
32 | super(new StatusImpl(status)); | ||
33 | } | ||
34 | |||
35 | } | ||
diff --git a/src/main/java/com/github/markozajc/akiwrapper/core/exceptions/StatusException.java b/src/main/java/com/github/markozajc/akiwrapper/core/exceptions/StatusException.java deleted file mode 100644 index 49b3062..0000000 --- a/src/main/java/com/github/markozajc/akiwrapper/core/exceptions/StatusException.java +++ /dev/null | |||
@@ -1,32 +0,0 @@ | |||
1 | package com.github.markozajc.akiwrapper.core.exceptions; | ||
2 | |||
3 | import com.github.markozajc.akiwrapper.core.entities.Status; | ||
4 | |||
5 | /** | ||
6 | * An exception indicating that the server returned an error code ("KO"). | ||
7 | * | ||
8 | * @author Marko Zajc | ||
9 | */ | ||
10 | public class StatusException extends RuntimeException { | ||
11 | |||
12 | private final Status status; | ||
13 | |||
14 | /** | ||
15 | * Constructs a new {@link StatusException}. | ||
16 | * | ||
17 | * @param status | ||
18 | * status to append | ||
19 | */ | ||
20 | public StatusException(Status status) { | ||
21 | super(status.getLevel().toString().toUpperCase() + " - " + status.getReason()); | ||
22 | this.status = status; | ||
23 | } | ||
24 | |||
25 | /** | ||
26 | * @return the problematic status that has been returned | ||
27 | */ | ||
28 | public Status getStatus() { | ||
29 | return this.status; | ||
30 | } | ||
31 | |||
32 | } | ||
diff --git a/src/main/java/com/github/markozajc/akiwrapper/core/exceptions/UndoOutOfBoundsException.java b/src/main/java/com/github/markozajc/akiwrapper/core/exceptions/UndoOutOfBoundsException.java new file mode 100644 index 0000000..d605fd3 --- /dev/null +++ b/src/main/java/com/github/markozajc/akiwrapper/core/exceptions/UndoOutOfBoundsException.java | |||
@@ -0,0 +1,16 @@ | |||
1 | package com.github.markozajc.akiwrapper.core.exceptions; | ||
2 | |||
3 | import com.github.markozajc.akiwrapper.Akiwrapper; | ||
4 | |||
5 | /** | ||
6 | * An exception indicating that {@link Akiwrapper#undoAnswer()} has been called on | ||
7 | * the first question - when {@link Akiwrapper#getStep()} is {@code 0}. | ||
8 | * | ||
9 | * @author Marko Zajc | ||
10 | */ | ||
11 | public class UndoOutOfBoundsException extends AkinatorException { | ||
12 | |||
13 | @SuppressWarnings("javadoc") // internal | ||
14 | public UndoOutOfBoundsException() {} | ||
15 | |||
16 | } | ||
diff --git a/src/main/java/com/github/markozajc/akiwrapper/core/impl/AkiwrapperImpl.java b/src/main/java/com/github/markozajc/akiwrapper/core/impl/AkiwrapperImpl.java index 7609cdb..00cad2c 100644 --- a/src/main/java/com/github/markozajc/akiwrapper/core/impl/AkiwrapperImpl.java +++ b/src/main/java/com/github/markozajc/akiwrapper/core/impl/AkiwrapperImpl.java | |||
@@ -1,46 +1,51 @@ | |||
1 | package com.github.markozajc.akiwrapper.core.impl; | 1 | package com.github.markozajc.akiwrapper.core.impl; |
2 | 2 | ||
3 | import static com.github.markozajc.akiwrapper.core.Route.*; | 3 | import static com.github.markozajc.akiwrapper.core.entities.Status.Reason.QUESTIONS_EXHAUSTED; |
4 | import static com.github.markozajc.akiwrapper.core.entities.Status.Level.ERROR; | 4 | import static com.github.markozajc.akiwrapper.core.utils.route.Routes.*; |
5 | import static com.github.markozajc.akiwrapper.core.entities.impl.immutable.StatusImpl.STATUS_OK; | ||
6 | import static java.lang.Integer.parseInt; | 5 | import static java.lang.Integer.parseInt; |
7 | import static java.lang.Long.parseLong; | 6 | import static java.lang.Long.parseLong; |
8 | import static java.lang.String.format; | 7 | import static java.lang.String.format; |
9 | import static java.lang.System.currentTimeMillis; | ||
10 | import static java.util.Collections.emptyList; | ||
11 | import static java.util.stream.Collectors.toUnmodifiableList; | 8 | import static java.util.stream.Collectors.toUnmodifiableList; |
12 | import static java.util.stream.StreamSupport.stream; | 9 | import static java.util.stream.StreamSupport.stream; |
13 | 10 | ||
14 | import java.util.List; | 11 | import java.util.List; |
15 | 12 | ||
16 | import javax.annotation.*; | 13 | import javax.annotation.Nonnull; |
17 | 14 | ||
15 | import org.eclipse.collections.api.factory.primitive.LongSets; | ||
16 | import org.eclipse.collections.api.set.primitive.MutableLongSet; | ||
18 | import org.json.JSONObject; | 17 | import org.json.JSONObject; |
18 | import org.slf4j.*; | ||
19 | 19 | ||
20 | import com.github.markozajc.akiwrapper.Akiwrapper; | 20 | import com.github.markozajc.akiwrapper.Akiwrapper; |
21 | import com.github.markozajc.akiwrapper.core.entities.*; | 21 | import com.github.markozajc.akiwrapper.core.entities.*; |
22 | import com.github.markozajc.akiwrapper.core.entities.impl.immutable.*; | 22 | import com.github.markozajc.akiwrapper.core.entities.impl.*; |
23 | import com.github.markozajc.akiwrapper.core.exceptions.*; | 23 | import com.github.markozajc.akiwrapper.core.exceptions.*; |
24 | import com.github.markozajc.akiwrapper.core.utils.ApiKey; | ||
24 | 25 | ||
25 | import kong.unirest.UnirestInstance; | 26 | import kong.unirest.UnirestInstance; |
26 | 27 | ||
28 | @SuppressWarnings("javadoc") // internal impl | ||
27 | public class AkiwrapperImpl implements Akiwrapper { | 29 | public class AkiwrapperImpl implements Akiwrapper { |
28 | 30 | ||
29 | private static final String NO_MORE_QUESTIONS_STATUS = "elem list is empty"; | ||
30 | private static final String PARAMETERS_KEY = "parameters"; | ||
31 | |||
32 | public static class Session { | 31 | public static class Session { |
33 | 32 | ||
34 | private static final String FORMAT_QUERYSTRING = "&session=%s&signature=%s"; | 33 | private static final String FORMAT_QUERYSTRING = "session=%s&signature=%s"; |
35 | 34 | ||
36 | private final long signature; | 35 | private final long signature; |
37 | private final int session; | 36 | private final int session; |
38 | 37 | ||
39 | public Session(long signature, int session) { | 38 | private Session(long signature, int session) { |
40 | this.signature = signature; | 39 | this.signature = signature; |
41 | this.session = session; | 40 | this.session = session; |
42 | } | 41 | } |
43 | 42 | ||
43 | @Nonnull | ||
44 | public static Session fromJson(@Nonnull JSONObject parameters) { | ||
45 | var session = parameters.getJSONObject("identification"); | ||
46 | return new Session(parseLong(session.getString("signature")), parseInt(session.getString("session"))); | ||
47 | } | ||
48 | |||
44 | public long getSignature() { | 49 | public long getSignature() { |
45 | return this.signature; | 50 | return this.signature; |
46 | } | 51 | } |
@@ -49,79 +54,134 @@ public class AkiwrapperImpl implements Akiwrapper { | |||
49 | return this.session; | 54 | return this.session; |
50 | } | 55 | } |
51 | 56 | ||
52 | public String querystring() { | 57 | public String asQuerystring() { |
53 | return format(FORMAT_QUERYSTRING, this.getSession(), this.getSignature()); | 58 | return format(FORMAT_QUERYSTRING, this.getSession(), this.getSignature()); |
54 | } | 59 | } |
55 | } | 60 | } |
56 | 61 | ||
57 | @Nonnull private final Server server; | 62 | private static final Logger LOG = LoggerFactory.getLogger(AkiwrapperImpl.class); |
63 | |||
64 | private static final int LAST_STEP = 80; | ||
65 | |||
66 | @Nonnull private final ServerImpl server; | ||
58 | @Nonnull private final UnirestInstance unirest; | 67 | @Nonnull private final UnirestInstance unirest; |
59 | private final boolean filterProfanity; | 68 | private final boolean filterProfanity; |
60 | @Nonnull private final Session session; | ||
61 | @Nonnegative private int currentStep; | ||
62 | @Nullable private Question question; | ||
63 | private List<Guess> guessCache; | ||
64 | |||
65 | @SuppressWarnings("null") | ||
66 | public AkiwrapperImpl(@Nonnull UnirestInstance unirest, @Nonnull Server server, boolean filterProfanity) { | ||
67 | var questionJson = | ||
68 | NEW_SESSION.createRequest(unirest, "", filterProfanity, currentTimeMillis(), server.getUrl()).getJSON(); | ||
69 | 69 | ||
70 | var parameters = questionJson.getJSONObject(PARAMETERS_KEY); | 70 | private ApiKey apiKey; |
71 | private Session session; | ||
72 | private Question question; | ||
73 | private int lastGuessStep; | ||
74 | private MutableLongSet rejectedGuesses = LongSets.mutable.empty(); | ||
71 | 75 | ||
72 | this.session = getSession(parameters); | 76 | public AkiwrapperImpl(@Nonnull UnirestInstance unirest, @Nonnull ServerImpl server, boolean filterProfanity) { |
73 | this.question = QuestionImpl.from(parameters.getJSONObject("step_information"), STATUS_OK); | ||
74 | this.filterProfanity = filterProfanity; | 77 | this.filterProfanity = filterProfanity; |
75 | this.server = server; | 78 | this.server = server; |
76 | this.unirest = unirest; | 79 | this.unirest = unirest; |
77 | this.currentStep = 0; | ||
78 | } | 80 | } |
79 | 81 | ||
80 | @SuppressWarnings("null") | 82 | @SuppressWarnings("null") |
83 | public void createSession() { | ||
84 | this.apiKey = ApiKey.accquireApiKey(this.unirest); | ||
85 | var sessionParameters = NEW_SESSION.createRequest(this).execute().getBody(); | ||
86 | this.session = Session.fromJson(sessionParameters); | ||
87 | this.question = QuestionImpl.fromJson(sessionParameters.getJSONObject("step_information")); | ||
88 | } | ||
89 | |||
81 | @Override | 90 | @Override |
82 | public Question answer(Answer answer) { | 91 | public Question answer(Answer answer) { |
83 | this.guessCache = null; | 92 | if (isExhausted()) |
84 | var oldQuestion = this.question; | 93 | throw new QuestionsExhaustedException(); |
85 | if (oldQuestion != null) { | 94 | |
86 | var newQuestionJson = ANSWER | 95 | var response = ANSWER.createRequest(this) |
87 | .createRequest(this.unirest, this.server.getUrl(), this.filterProfanity, this.session, | 96 | .parameter(PARAMETER_STEP, getStep()) |
88 | oldQuestion.getStep(), answer.getId()) | 97 | .parameter(PARAMETER_ANSWER, answer.getId()) |
89 | .getJSON(); | 98 | .execute(); |
90 | |||
91 | try { | ||
92 | this.question = | ||
93 | QuestionImpl.from(newQuestionJson.getJSONObject(PARAMETERS_KEY), new StatusImpl(newQuestionJson)); | ||
94 | |||
95 | } catch (MissingQuestionException e) { // NOSONAR It does not need to be logged | ||
96 | this.question = null; | ||
97 | return null; | ||
98 | } | ||
99 | 99 | ||
100 | this.currentStep += 1; | 100 | if (response.getStatus().getReason() == QUESTIONS_EXHAUSTED) { |
101 | return this.question; | 101 | return this.question = null; |
102 | } | 102 | } |
103 | 103 | ||
104 | return null; | 104 | return this.question = QuestionImpl.fromJson(response.getBody()); |
105 | } | 105 | } |
106 | 106 | ||
107 | @Override | 107 | @Override |
108 | @SuppressWarnings("null") | 108 | @SuppressWarnings("null") |
109 | public Question undoAnswer() { | 109 | public Question undoAnswer() { |
110 | this.guessCache = null; | 110 | if (isExhausted()) |
111 | throw new QuestionsExhaustedException(); // the api won't let us | ||
111 | 112 | ||
112 | Question current = getQuestion(); | 113 | if (getStep() == 0) |
113 | if (current == null || current.getStep() < 1) | 114 | throw new UndoOutOfBoundsException(); |
115 | |||
116 | var response = CANCEL_ANSWER.createRequest(this).parameter(PARAMETER_STEP, getStep()).execute(); | ||
117 | return this.question = QuestionImpl.fromJson(response.getBody()); | ||
118 | } | ||
119 | |||
120 | @Override | ||
121 | @SuppressWarnings("null") | ||
122 | public List<Guess> getGuesses(int count) { | ||
123 | var request = LIST.createRequest(this).parameter(PARAMETER_STEP, getStep()); | ||
124 | if (count > 0) | ||
125 | request.parameter(PARAMETER_SIZE, count); | ||
126 | var response = request.execute(); | ||
127 | |||
128 | return stream(response.getBody().getJSONArray("elements").spliterator(), false).map(JSONObject.class::cast) | ||
129 | .map(j -> j.getJSONObject("element")) | ||
130 | .map(GuessImpl::fromJson) | ||
131 | .sorted() | ||
132 | .collect(toUnmodifiableList()); | ||
133 | } | ||
134 | |||
135 | @Override | ||
136 | public Guess suggestGuess() { | ||
137 | boolean shouldSuggest = isExhausted() || getStep() - this.lastGuessStep >= 5 // NOSONAR I'm just copying | ||
138 | && (this.question != null && this.question.getProgression() > 97 || getStep() - this.lastGuessStep == 25) | ||
139 | && getStep() != 75; | ||
140 | // I have no clue what that last part is, but that's what akinator does | ||
141 | |||
142 | if (!shouldSuggest) | ||
114 | return null; | 143 | return null; |
115 | 144 | ||
116 | var questionJson = CANCEL_ANSWER | 145 | for (var guess : getGuesses(2)) { |
117 | .createRequest(this.unirest, this.server.getUrl(), this.filterProfanity, this.session, current.getStep()) | 146 | if (!this.rejectedGuesses.contains(guess.getIdLong())) { |
118 | .getJSON(); | 147 | this.rejectedGuesses.add(guess.getIdLong()); |
148 | this.lastGuessStep = getStep(); | ||
149 | return guess; | ||
150 | } | ||
151 | } | ||
152 | return null; | ||
153 | } | ||
119 | 154 | ||
120 | this.question = QuestionImpl.from(questionJson.getJSONObject(PARAMETERS_KEY), new StatusImpl(questionJson)); | 155 | @Override |
156 | public Question rejectLastGuess() { | ||
157 | try { | ||
158 | var response = EXCLUSION.createRequest(this).parameter(PARAMETER_STEP, getStep()).execute(); | ||
159 | return this.question = QuestionImpl.fromJson(response.getBody()); | ||
121 | 160 | ||
122 | this.currentStep -= 1; | 161 | } catch (AkinatorException e) { |
162 | if (isExhausted()) { | ||
163 | // we don't care about out session anymore anyways, throwing would be silly | ||
164 | LOG.warn("Caught an exception when rejecting a guess", e); | ||
165 | return null; | ||
166 | |||
167 | } else { | ||
168 | throw e; | ||
169 | } | ||
170 | } | ||
123 | 171 | ||
124 | return this.question; | 172 | } |
173 | |||
174 | @Override | ||
175 | public void confirmGuess(Guess guess) { | ||
176 | try { | ||
177 | CHOICE.createRequest(this) | ||
178 | .parameter(PARAMETER_STEP, getStep()) | ||
179 | .parameter(PARAMETER_ELEMENT, guess.getId()) | ||
180 | .execute(); | ||
181 | } catch (AkinatorException e) { | ||
182 | // we don't care about out session anymore anyways, throwing would be silly | ||
183 | LOG.warn("Caught an exception when confirming a guess", e); | ||
184 | } | ||
125 | } | 185 | } |
126 | 186 | ||
127 | @Override | 187 | @Override |
@@ -129,44 +189,39 @@ public class AkiwrapperImpl implements Akiwrapper { | |||
129 | return this.question; | 189 | return this.question; |
130 | } | 190 | } |
131 | 191 | ||
132 | @SuppressWarnings("null") | ||
133 | @Override | 192 | @Override |
134 | public List<Guess> getGuesses() { | 193 | public int getStep() { |
135 | try { | 194 | var question = this.getQuestion(); |
136 | if (this.guessCache == null) | 195 | return question == null ? LAST_STEP : question.getStep(); |
137 | this.guessCache = stream(LIST | 196 | } |
138 | .createRequest(this.unirest, this.server.getUrl(), this.filterProfanity, this.session, | ||
139 | this.currentStep) | ||
140 | .getJSON() | ||
141 | .getJSONObject(PARAMETERS_KEY) | ||
142 | .getJSONArray("elements") | ||
143 | .spliterator(), false).map(JSONObject.class::cast) | ||
144 | .map(j -> j.getJSONObject("element")) | ||
145 | .map(GuessImpl::from) | ||
146 | .sorted() | ||
147 | .collect(toUnmodifiableList()); | ||
148 | |||
149 | return this.guessCache; | ||
150 | |||
151 | } catch (StatusException e) { | ||
152 | if (e.getStatus().getLevel() == ERROR | ||
153 | && NO_MORE_QUESTIONS_STATUS.equalsIgnoreCase(e.getStatus().getReason())) { | ||
154 | return emptyList(); | ||
155 | } | ||
156 | 197 | ||
157 | throw e; | 198 | @Override |
158 | } | 199 | public boolean isExhausted() { |
200 | // question is only null after we've exhausted them (that is post step 80) | ||
201 | return this.question == null; | ||
159 | } | 202 | } |
160 | 203 | ||
161 | @Override | 204 | @Override |
162 | public Server getServer() { | 205 | public ServerImpl getServer() { |
163 | return this.server; | 206 | return this.server; |
164 | } | 207 | } |
165 | 208 | ||
209 | @Override | ||
210 | public boolean doesFilterProfanity() { | ||
211 | return this.filterProfanity; | ||
212 | } | ||
213 | |||
214 | public Session getSession() { | ||
215 | return this.session; | ||
216 | } | ||
217 | |||
218 | public ApiKey getApiKey() { | ||
219 | return this.apiKey; | ||
220 | } | ||
221 | |||
166 | @Nonnull | 222 | @Nonnull |
167 | private static Session getSession(@Nonnull JSONObject parameters) { | 223 | public UnirestInstance getUnirest() { |
168 | var session = parameters.getJSONObject("identification"); | 224 | return this.unirest; |
169 | return new Session(parseLong(session.getString("signature")), parseInt(session.getString("session"))); | ||
170 | } | 225 | } |
171 | 226 | ||
172 | } | 227 | } |
diff --git a/src/main/java/com/github/markozajc/akiwrapper/core/entities/impl/immutable/ApiKey.java b/src/main/java/com/github/markozajc/akiwrapper/core/utils/ApiKey.java index 5447ab4..bfe2829 100644 --- a/src/main/java/com/github/markozajc/akiwrapper/core/entities/impl/immutable/ApiKey.java +++ b/src/main/java/com/github/markozajc/akiwrapper/core/utils/ApiKey.java | |||
@@ -1,53 +1,54 @@ | |||
1 | package com.github.markozajc.akiwrapper.core.entities.impl.immutable; | 1 | package com.github.markozajc.akiwrapper.core.utils; |
2 | 2 | ||
3 | import static com.github.markozajc.akiwrapper.core.Route.BASE_AKINATOR_URL; | ||
4 | import static java.lang.String.format; | 3 | import static java.lang.String.format; |
5 | import static java.nio.charset.StandardCharsets.UTF_8; | 4 | import static java.nio.charset.StandardCharsets.UTF_8; |
6 | import static java.util.regex.Pattern.compile; | 5 | import static java.util.regex.Pattern.compile; |
7 | 6 | ||
8 | import java.net.URLEncoder; | 7 | import java.net.URLEncoder; |
9 | import java.util.Base64; | ||
10 | import java.util.regex.Pattern; | 8 | import java.util.regex.Pattern; |
11 | 9 | ||
12 | import javax.annotation.Nonnull; | 10 | import javax.annotation.Nonnull; |
13 | 11 | ||
12 | import com.github.markozajc.akiwrapper.core.utils.route.Route; | ||
13 | |||
14 | import kong.unirest.UnirestInstance; | 14 | import kong.unirest.UnirestInstance; |
15 | 15 | ||
16 | @SuppressWarnings("javadoc") // internal impl | ||
16 | public class ApiKey { | 17 | public class ApiKey { |
17 | 18 | ||
18 | private static final String EXCEPTION_NO_KEY = "Couldn't find the API key!" + | 19 | private static final String EXCEPTION_NO_KEY = "Couldn't find the API key!" + |
19 | "Please consider opening a new ticket at https://github.com/markozajc/Akiwrapper/issues." + | 20 | "Please consider opening a new ticket at https://github.com/markozajc/Akiwrapper/issues."; |
20 | "Base64 encoded page: %s"; | ||
21 | 21 | ||
22 | private static final Pattern API_KEY_PATTERN = | 22 | private static final Pattern API_KEY_PATTERN = |
23 | compile("var uid_ext_session = '(.*)'\\;\\n.*var frontaddr = '(.*)'\\;"); | 23 | compile("var uid_ext_session = '(.*)'\\;\\n.*var frontaddr = '(.*)'\\;"); |
24 | 24 | ||
25 | private static final String FORMAT = "frontaddr=%s&uid_ext_session=%s"; | 25 | @Nonnull private final String uidExtSession; |
26 | 26 | @Nonnull private final String frontaddr; | |
27 | @Nonnull private final String sessionUid; | ||
28 | @Nonnull private final String frontAddress; | ||
29 | 27 | ||
30 | ApiKey(@Nonnull String sessionUid, @Nonnull String frontAddress) { | 28 | ApiKey(@Nonnull String sessionUid, @Nonnull String frontAddress) { |
31 | this.sessionUid = sessionUid; | 29 | this.uidExtSession = sessionUid; |
32 | this.frontAddress = frontAddress; | 30 | this.frontaddr = frontAddress; |
33 | } | 31 | } |
34 | 32 | ||
35 | @Nonnull | 33 | @Nonnull |
36 | @SuppressWarnings("null") | 34 | public String asQuerystringUidExtSession() { |
37 | public String querystring() { | 35 | return "uid_ext_session=" + this.uidExtSession; |
38 | return format(FORMAT, URLEncoder.encode(this.frontAddress, UTF_8), this.sessionUid); | 36 | } |
37 | |||
38 | @Nonnull | ||
39 | public String asQuerystringFrontaddr() { | ||
40 | return "frontaddr=" + URLEncoder.encode(this.frontaddr, UTF_8); | ||
39 | } | 41 | } |
40 | 42 | ||
41 | @SuppressWarnings("null") | 43 | @SuppressWarnings("null") |
42 | public static ApiKey accquireApiKey(UnirestInstance unirest) { | 44 | public static ApiKey accquireApiKey(UnirestInstance unirest) { |
43 | var page = unirest.get(BASE_AKINATOR_URL + "/game").asString().getBody(); | 45 | var page = unirest.get(Route.WEBSITE_URL + "/game").asString().getBody(); |
44 | var matcher = API_KEY_PATTERN.matcher(page); | 46 | var matcher = API_KEY_PATTERN.matcher(page); |
45 | if (matcher.find()) { | 47 | if (matcher.find()) { |
46 | return new ApiKey(matcher.group(1), matcher.group(2)); | 48 | return new ApiKey(matcher.group(1), matcher.group(2)); |
47 | 49 | ||
48 | } else { | 50 | } else { |
49 | throw new IllegalStateException(format(EXCEPTION_NO_KEY, | 51 | throw new IllegalStateException(format(EXCEPTION_NO_KEY)); |
50 | Base64.getEncoder().encodeToString(page.getBytes(UTF_8)))); | ||
51 | } | 52 | } |
52 | } | 53 | } |
53 | 54 | ||
diff --git a/src/main/java/com/github/markozajc/akiwrapper/core/utils/Servers.java b/src/main/java/com/github/markozajc/akiwrapper/core/utils/Servers.java index 6f74a9b..528b671 100644 --- a/src/main/java/com/github/markozajc/akiwrapper/core/utils/Servers.java +++ b/src/main/java/com/github/markozajc/akiwrapper/core/utils/Servers.java | |||
@@ -7,10 +7,9 @@ import java.util.stream.Stream; | |||
7 | 7 | ||
8 | import javax.annotation.Nonnull; | 8 | import javax.annotation.Nonnull; |
9 | 9 | ||
10 | import com.github.markozajc.akiwrapper.core.entities.*; | 10 | import com.github.markozajc.akiwrapper.core.entities.Server; |
11 | import com.github.markozajc.akiwrapper.core.entities.Server.*; | 11 | import com.github.markozajc.akiwrapper.core.entities.Server.*; |
12 | import com.github.markozajc.akiwrapper.core.entities.impl.immutable.*; | 12 | import com.github.markozajc.akiwrapper.core.entities.impl.ServerImpl; |
13 | import com.github.markozajc.akiwrapper.core.exceptions.ServerNotFoundException; | ||
14 | import com.jcabi.xml.XMLDocument; | 13 | import com.jcabi.xml.XMLDocument; |
15 | 14 | ||
16 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; | 15 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; |
@@ -30,8 +29,7 @@ public final class Servers { | |||
30 | private Servers() {} | 29 | private Servers() {} |
31 | 30 | ||
32 | /** | 31 | /** |
33 | * Finds correct {@link Server}s using given parameters and compiles a | 32 | * Finds correct {@link Server}s using given parameters |
34 | * {@link ServerList} out of them. | ||
35 | * | 33 | * |
36 | * @param unirest | 34 | * @param unirest |
37 | * the {@link UnirestInstance} to use for the request | 35 | * the {@link UnirestInstance} to use for the request |
@@ -40,20 +38,15 @@ public final class Servers { | |||
40 | * @param guessType | 38 | * @param guessType |
41 | * guessType of the server to search for | 39 | * guessType of the server to search for |
42 | * | 40 | * |
43 | * @return a {@link ServerList} with {@link Server}s that suit the given parameters. | 41 | * @return a list of {@link Server}s that suit the given parameters. |
44 | * | ||
45 | * @throws ServerNotFoundException | ||
46 | * if there is no server that matches the query. | ||
47 | */ | 42 | */ |
48 | @Nonnull | 43 | @Nonnull |
49 | public static ServerList findServers(@Nonnull UnirestInstance unirest, @Nonnull Language localization, | 44 | @SuppressWarnings("null") |
50 | @Nonnull GuessType guessType) throws ServerNotFoundException { | 45 | public static List<ServerImpl> findServers(@Nonnull UnirestInstance unirest, @Nonnull Language localization, |
51 | List<Server> servers = getServers(unirest).filter(s -> s.getGuessType() == guessType) | 46 | @Nonnull GuessType guessType) { |
47 | return getServers(unirest).filter(s -> s.getGuessType() == guessType) | ||
52 | .filter(s -> s.getLanguage() == localization) | 48 | .filter(s -> s.getLanguage() == localization) |
53 | .collect(toList()); | 49 | .collect(toList()); |
54 | if (servers.isEmpty()) | ||
55 | throw new ServerNotFoundException(); | ||
56 | return new ServerListImpl(servers); | ||
57 | } | 50 | } |
58 | 51 | ||
59 | /** | 52 | /** |
@@ -63,10 +56,10 @@ public final class Servers { | |||
63 | * @param unirest | 56 | * @param unirest |
64 | * the {@link UnirestInstance} to use for the request | 57 | * the {@link UnirestInstance} to use for the request |
65 | * | 58 | * |
66 | * @return a {@link Stream} of all {@link Server}s. | 59 | * @return a {@link Stream} of all available {@link Server}s. |
67 | */ | 60 | */ |
68 | @SuppressWarnings("null") | 61 | @SuppressWarnings("null") |
69 | public static Stream<Server> getServers(@Nonnull UnirestInstance unirest) { | 62 | public static Stream<ServerImpl> getServers(@Nonnull UnirestInstance unirest) { |
70 | return new XMLDocument(fetchListXml(unirest)).nodes("//RESULT/PARAMETERS/*") | 63 | return new XMLDocument(fetchListXml(unirest)).nodes("//RESULT/PARAMETERS/*") |
71 | .stream() | 64 | .stream() |
72 | .flatMap(xml -> ServerImpl.fromXml(xml).stream()); | 65 | .flatMap(xml -> ServerImpl.fromXml(xml).stream()); |
diff --git a/src/main/java/com/github/markozajc/akiwrapper/core/utils/UnirestUtils.java b/src/main/java/com/github/markozajc/akiwrapper/core/utils/UnirestUtils.java index f418cb9..7dc38e3 100644 --- a/src/main/java/com/github/markozajc/akiwrapper/core/utils/UnirestUtils.java +++ b/src/main/java/com/github/markozajc/akiwrapper/core/utils/UnirestUtils.java | |||
@@ -48,15 +48,16 @@ public class UnirestUtils { | |||
48 | } | 48 | } |
49 | 49 | ||
50 | /** | 50 | /** |
51 | * Configures a new {@link UnirestInstance} for use by Akiwrapper. Akinator's API | 51 | * <b>Note:</b> even though this method returns a {@link UnirestInstance}, the |
52 | * servers are quite picky about the headers you send to them so if you supply | 52 | * instance you pass to it is itself mutated and returned. The return value is only |
53 | * {@link AkiwrapperBuilder} with your own {@link UnirestInstance} you should either | 53 | * there for ease of chaining. Configures a new {@link UnirestInstance} for use by |
54 | * pass it through this or configure it accordingly yourself. This also applies the | 54 | * Akiwrapper.<br> |
55 | * workaround to Akinator's incomplete SSL chain from | 55 | * <br> |
56 | * {@link WorkaroundUtils#workaroundIncompleteChain(kong.unirest.Config)} .<br> | 56 | * Akinator's API servers are quite picky about the headers you send to them so if |
57 | * Note: even though this method returns a {@link UnirestInstance}, the instance you | 57 | * you supply {@link AkiwrapperBuilder} with your own {@link UnirestInstance} you |
58 | * pass to it is itself mutated and returned. The return value is only there for ease | 58 | * should either pass it through this or configure it accordingly yourself. This also |
59 | * of chaining. | 59 | * applies the workaround to Akinator's incomplete SSL chain from |
60 | * {@link WorkaroundUtils#workaroundIncompleteChain(kong.unirest.Config)}. | ||
60 | * | 61 | * |
61 | * @param unirest | 62 | * @param unirest |
62 | * the {@link UnirestInstance} to configure | 63 | * the {@link UnirestInstance} to configure |
diff --git a/src/main/java/com/github/markozajc/akiwrapper/core/utils/WorkaroundUtils.java b/src/main/java/com/github/markozajc/akiwrapper/core/utils/WorkaroundUtils.java index 0cc1427..71620dd 100644 --- a/src/main/java/com/github/markozajc/akiwrapper/core/utils/WorkaroundUtils.java +++ b/src/main/java/com/github/markozajc/akiwrapper/core/utils/WorkaroundUtils.java | |||
@@ -9,6 +9,7 @@ import java.security.cert.*; | |||
9 | import javax.annotation.Nonnull; | 9 | import javax.annotation.Nonnull; |
10 | import javax.net.ssl.*; | 10 | import javax.net.ssl.*; |
11 | 11 | ||
12 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; | ||
12 | import kong.unirest.Config; | 13 | import kong.unirest.Config; |
13 | 14 | ||
14 | /** | 15 | /** |
@@ -19,11 +20,12 @@ import kong.unirest.Config; | |||
19 | public class WorkaroundUtils { | 20 | public class WorkaroundUtils { |
20 | 21 | ||
21 | /** | 22 | /** |
23 | * <b>Note:</b> even though this method returns a {@link Config}, the instance you | ||
24 | * pass to it is itself mutated and returned. The return value is only there for ease | ||
25 | * of chaining.<br> | ||
26 | * <br> | ||
22 | * Applies a workaround for the {@code PKIX path building failed} exception to a | 27 | * Applies a workaround for the {@code PKIX path building failed} exception to a |
23 | * Unirest {@link Config}.<br> | 28 | * Unirest {@link Config}. |
24 | * Note: even though this method returns a {@link Config}, the instance you pass to | ||
25 | * it is itself mutated and returned. The return value is only there for ease of | ||
26 | * chaining. | ||
27 | * | 29 | * |
28 | * @param config | 30 | * @param config |
29 | * the {@link Config} to apply the workaround to | 31 | * the {@link Config} to apply the workaround to |
@@ -110,6 +112,7 @@ public class WorkaroundUtils { | |||
110 | */ | 112 | */ |
111 | @Nonnull | 113 | @Nonnull |
112 | @SuppressWarnings("null") | 114 | @SuppressWarnings("null") |
115 | @SuppressFBWarnings("HARD_CODE_PASSWORD") | ||
113 | public static X509TrustManager getIncompleteChainWorkaroundCustomTrustManager() throws KeyStoreException, | 116 | public static X509TrustManager getIncompleteChainWorkaroundCustomTrustManager() throws KeyStoreException, |
114 | IOException, | 117 | IOException, |
115 | NoSuchAlgorithmException, | 118 | NoSuchAlgorithmException, |
diff --git a/src/main/java/com/github/markozajc/akiwrapper/core/utils/route/Request.java b/src/main/java/com/github/markozajc/akiwrapper/core/utils/route/Request.java new file mode 100644 index 0000000..78980d9 --- /dev/null +++ b/src/main/java/com/github/markozajc/akiwrapper/core/utils/route/Request.java | |||
@@ -0,0 +1,106 @@ | |||
1 | package com.github.markozajc.akiwrapper.core.utils.route; | ||
2 | |||
3 | import static com.github.markozajc.akiwrapper.core.entities.Status.Level.ERROR; | ||
4 | import static com.github.markozajc.akiwrapper.core.utils.route.Route.*; | ||
5 | import static java.util.stream.Collectors.joining; | ||
6 | import static org.slf4j.LoggerFactory.getLogger; | ||
7 | |||
8 | import java.util.*; | ||
9 | |||
10 | import javax.annotation.*; | ||
11 | |||
12 | import org.json.*; | ||
13 | import org.slf4j.Logger; | ||
14 | |||
15 | import com.github.markozajc.akiwrapper.core.entities.impl.StatusImpl; | ||
16 | import com.github.markozajc.akiwrapper.core.exceptions.*; | ||
17 | |||
18 | import kong.unirest.UnirestInstance; | ||
19 | |||
20 | @SuppressWarnings("javadoc") // internal util | ||
21 | public class Request { | ||
22 | |||
23 | private static final Logger LOG = getLogger(Request.class); | ||
24 | |||
25 | @Nonnull private final String url; | ||
26 | @Nonnull private final UnirestInstance unirest; | ||
27 | private Set<String> mandatoryParameters; | ||
28 | private Map<String, String> parameters; | ||
29 | private final boolean urlHasQuerystring; | ||
30 | |||
31 | Request(@Nonnull String url, @Nonnull UnirestInstance unirest, @Nullable Set<String> mandatoryParameters, | ||
32 | @Nullable Map<String, String> parameters, boolean pathHasQuerystring) { | ||
33 | this.url = url; | ||
34 | this.unirest = unirest; | ||
35 | this.urlHasQuerystring = pathHasQuerystring; | ||
36 | |||
37 | if (mandatoryParameters != null) | ||
38 | this.mandatoryParameters = new HashSet<>(mandatoryParameters); | ||
39 | else | ||
40 | this.mandatoryParameters = null; | ||
41 | |||
42 | if (parameters != null) | ||
43 | this.parameters = new HashMap<>(parameters); | ||
44 | else | ||
45 | this.parameters = null; | ||
46 | } | ||
47 | |||
48 | @Nonnull | ||
49 | @SuppressWarnings("null") | ||
50 | public Request parameter(@Nonnull String name, int value) { | ||
51 | parameter(name, Integer.toString(value)); | ||
52 | return this; | ||
53 | } | ||
54 | |||
55 | @Nonnull | ||
56 | public Request parameter(@Nonnull String name, @Nonnull String value) { | ||
57 | if (this.parameters != null && this.parameters.containsKey(name)) { | ||
58 | this.parameters.put(name, value); | ||
59 | |||
60 | if (this.mandatoryParameters != null) | ||
61 | this.mandatoryParameters.remove(name); | ||
62 | |||
63 | } else { | ||
64 | throw new IllegalArgumentException("Parameter \"" + name + "\" is not defined"); | ||
65 | } | ||
66 | return this; | ||
67 | } | ||
68 | |||
69 | @Nonnull | ||
70 | @SuppressWarnings("null") | ||
71 | public Response execute() { | ||
72 | checkState(); | ||
73 | |||
74 | String processedUrl = this.url; | ||
75 | boolean hasQuerystring = this.urlHasQuerystring; | ||
76 | |||
77 | if (this.parameters != null && !this.parameters.isEmpty()) | ||
78 | processedUrl += formatQuerystring(formatParameters(this.parameters), hasQuerystring); | ||
79 | |||
80 | LOG.trace("--> {}", processedUrl); | ||
81 | var response = this.unirest.get(processedUrl).asString().getBody(); | ||
82 | |||
83 | LOG.trace("<-- {}", response); | ||
84 | response = response.substring(7 /* "jQuery(" */, response.length() - 1 /* ")" */); // cut the callback | ||
85 | |||
86 | try { | ||
87 | var body = new JSONObject(response); | ||
88 | var status = StatusImpl.fromJson(body); | ||
89 | if (status.getLevel() == ERROR) | ||
90 | throw new ServerStatusException(status); | ||
91 | |||
92 | return new Response(status, body); | ||
93 | |||
94 | } catch (JSONException e) { | ||
95 | throw new AkinatorException("Couldn't parse a server response", e); | ||
96 | } | ||
97 | } | ||
98 | |||
99 | private void checkState() { | ||
100 | if (this.mandatoryParameters != null && !this.mandatoryParameters.isEmpty()) { | ||
101 | var unset = this.mandatoryParameters.stream().collect(joining(", ")); | ||
102 | throw new IllegalStateException("Some mandatory parameters aren't set: " + unset); | ||
103 | } | ||
104 | } | ||
105 | |||
106 | } | ||
diff --git a/src/main/java/com/github/markozajc/akiwrapper/core/utils/route/Response.java b/src/main/java/com/github/markozajc/akiwrapper/core/utils/route/Response.java new file mode 100644 index 0000000..08d4714 --- /dev/null +++ b/src/main/java/com/github/markozajc/akiwrapper/core/utils/route/Response.java | |||
@@ -0,0 +1,31 @@ | |||
1 | package com.github.markozajc.akiwrapper.core.utils.route; | ||
2 | |||
3 | import javax.annotation.Nonnull; | ||
4 | |||
5 | import org.json.JSONObject; | ||
6 | |||
7 | import com.github.markozajc.akiwrapper.core.entities.Status; | ||
8 | |||
9 | @SuppressWarnings("javadoc") // internal util | ||
10 | public class Response { | ||
11 | |||
12 | @Nonnull private final Status status; | ||
13 | @Nonnull private final JSONObject body; | ||
14 | |||
15 | public Response(@Nonnull Status status, @Nonnull JSONObject body) { | ||
16 | this.status = status; | ||
17 | this.body = body; | ||
18 | } | ||
19 | |||
20 | @Nonnull | ||
21 | public Status getStatus() { | ||
22 | return this.status; | ||
23 | } | ||
24 | |||
25 | @Nonnull | ||
26 | @SuppressWarnings("null") | ||
27 | public JSONObject getBody() { | ||
28 | return this.body.getJSONObject("parameters"); | ||
29 | } | ||
30 | |||
31 | } | ||
diff --git a/src/main/java/com/github/markozajc/akiwrapper/core/utils/route/Route.java b/src/main/java/com/github/markozajc/akiwrapper/core/utils/route/Route.java new file mode 100644 index 0000000..33e25b9 --- /dev/null +++ b/src/main/java/com/github/markozajc/akiwrapper/core/utils/route/Route.java | |||
@@ -0,0 +1,137 @@ | |||
1 | package com.github.markozajc.akiwrapper.core.utils.route; | ||
2 | |||
3 | import static com.github.markozajc.akiwrapper.core.utils.route.Route.Endpoint.GAME_SERVER; | ||
4 | import static java.lang.String.format; | ||
5 | import static java.nio.charset.StandardCharsets.UTF_8; | ||
6 | import static java.util.stream.Collectors.joining; | ||
7 | |||
8 | import java.net.URLEncoder; | ||
9 | import java.util.*; | ||
10 | import java.util.function.Supplier; | ||
11 | |||
12 | import javax.annotation.*; | ||
13 | |||
14 | import com.github.markozajc.akiwrapper.core.impl.AkiwrapperImpl; | ||
15 | |||
16 | @SuppressWarnings("javadoc") // internal util | ||
17 | public final class Route { | ||
18 | |||
19 | public static final String WEBSITE_URL = "https://en.akinator.com"; | ||
20 | |||
21 | @Nonnull private final String path; | ||
22 | @Nonnull private final Endpoint endpoint; | ||
23 | private final boolean requiresSession; | ||
24 | private final boolean requiresFrontaddr; | ||
25 | private final boolean requiresUidExtSession; | ||
26 | private final boolean requiresUrlApiWs; | ||
27 | private final Map<String, Supplier<String>> automaticParameters; | ||
28 | @Nullable private final String profanityDisabledQuerystring; | ||
29 | @Nullable private final String profanityEnabledQuerystring; | ||
30 | @Nullable private Set<String> mandatoryParameters; | ||
31 | @Nullable private Map<String, String> parameters; | ||
32 | private boolean pathHasQuerystring; | ||
33 | |||
34 | Route(@Nonnull String path, @Nonnull Endpoint endpoint, boolean requiresSession, boolean requiresFrontaddr, | ||
35 | boolean requiresUidExtSession, boolean requiresUrlApiWs, | ||
36 | @Nullable Map<String, Supplier<String>> automaticParameters, @Nullable String profanityDisabledQuerystring, | ||
37 | @Nullable String profanityEnabledQuerystring, @Nullable Set<String> mandatoryParameters, | ||
38 | @Nullable Map<String, String> parameters, boolean pathHasQuerystring) { | ||
39 | this.path = path; | ||
40 | this.endpoint = endpoint; | ||
41 | this.requiresSession = requiresSession; | ||
42 | this.requiresFrontaddr = requiresFrontaddr; | ||
43 | this.requiresUidExtSession = requiresUidExtSession; | ||
44 | this.requiresUrlApiWs = requiresUrlApiWs; | ||
45 | this.automaticParameters = automaticParameters; | ||
46 | this.profanityDisabledQuerystring = profanityDisabledQuerystring; | ||
47 | this.profanityEnabledQuerystring = profanityEnabledQuerystring; | ||
48 | this.mandatoryParameters = mandatoryParameters; | ||
49 | this.parameters = parameters; | ||
50 | this.pathHasQuerystring = pathHasQuerystring; | ||
51 | } | ||
52 | |||
53 | @Nonnull | ||
54 | @SuppressWarnings({ "resource", "null" }) | ||
55 | public Request createRequest(@Nonnull AkiwrapperImpl api) { | ||
56 | boolean hasQuerystring = this.pathHasQuerystring; | ||
57 | |||
58 | var url = new StringBuilder(); | ||
59 | if (this.endpoint == GAME_SERVER) { | ||
60 | url.append(api.getServer().getUrl()); | ||
61 | } else { | ||
62 | url.append(WEBSITE_URL); | ||
63 | } | ||
64 | |||
65 | url.append(this.path); | ||
66 | |||
67 | // generate and append automatic parameters | ||
68 | if (this.automaticParameters != null && !this.automaticParameters.isEmpty()) { | ||
69 | url.append(formatQuerystring(formatParameters(this.automaticParameters), hasQuerystring)); | ||
70 | hasQuerystring = true; | ||
71 | } | ||
72 | |||
73 | // append urlApiWs | ||
74 | if (this.requiresUrlApiWs) { | ||
75 | url.append(formatQuerystring(api.getServer().asUrlApiWs(), hasQuerystring)); | ||
76 | hasQuerystring = true; | ||
77 | } | ||
78 | |||
79 | // append session | ||
80 | if (this.requiresSession) { | ||
81 | if (api.getSession() == null) | ||
82 | throw new IllegalStateException("Session is required but not set in the Akiwrapper object"); | ||
83 | |||
84 | url.append(formatQuerystring(api.getSession().asQuerystring(), hasQuerystring)); | ||
85 | hasQuerystring = true; | ||
86 | } | ||
87 | |||
88 | // append frontaddr (of api key) | ||
89 | if (this.requiresFrontaddr) { | ||
90 | url.append(formatQuerystring(api.getApiKey().asQuerystringFrontaddr(), hasQuerystring)); | ||
91 | hasQuerystring = true; | ||
92 | } | ||
93 | |||
94 | // append uid_ext_session (of api key) | ||
95 | if (this.requiresUidExtSession) { | ||
96 | url.append(formatQuerystring(api.getApiKey().asQuerystringUidExtSession(), hasQuerystring)); | ||
97 | hasQuerystring = true; | ||
98 | } | ||
99 | |||
100 | // append profanity parameters | ||
101 | if (api.doesFilterProfanity()) { | ||
102 | if (this.profanityDisabledQuerystring != null) { | ||
103 | url.append(formatQuerystring(this.profanityDisabledQuerystring, hasQuerystring)); | ||
104 | hasQuerystring = true; | ||
105 | } | ||
106 | |||
107 | } else if (this.profanityEnabledQuerystring != null) { | ||
108 | url.append(formatQuerystring(this.profanityEnabledQuerystring, hasQuerystring)); | ||
109 | hasQuerystring = true; | ||
110 | } | ||
111 | |||
112 | return new Request(url.toString(), api.getUnirest(), this.mandatoryParameters, this.parameters, hasQuerystring); | ||
113 | } | ||
114 | |||
115 | @Nonnull | ||
116 | static String formatQuerystring(@Nonnull String querystring, boolean pathHasQuerystring) { | ||
117 | return (pathHasQuerystring ? '&' : '?') + querystring; | ||
118 | } | ||
119 | |||
120 | @Nonnull | ||
121 | @SuppressWarnings("null") | ||
122 | static String formatParameters(@Nonnull Map<String, ?> parameters) { | ||
123 | return parameters.entrySet().stream().filter(e -> e.getValue() != null).map(e -> { | ||
124 | Object value = e.getValue(); | ||
125 | if (value instanceof Supplier) | ||
126 | value = ((Supplier<?>) value).get(); | ||
127 | |||
128 | return format("%s=%s", e.getKey(), URLEncoder.encode(value.toString(), UTF_8)); | ||
129 | }).collect(joining("&")); | ||
130 | } | ||
131 | |||
132 | enum Endpoint { | ||
133 | WEBSITE, // en.akinator.com | ||
134 | GAME_SERVER // srvX.akinator.com | ||
135 | } | ||
136 | |||
137 | } | ||
diff --git a/src/main/java/com/github/markozajc/akiwrapper/core/utils/route/RouteBuilder.java b/src/main/java/com/github/markozajc/akiwrapper/core/utils/route/RouteBuilder.java new file mode 100644 index 0000000..225cc1a --- /dev/null +++ b/src/main/java/com/github/markozajc/akiwrapper/core/utils/route/RouteBuilder.java | |||
@@ -0,0 +1,150 @@ | |||
1 | package com.github.markozajc.akiwrapper.core.utils.route; | ||
2 | |||
3 | import static com.github.markozajc.akiwrapper.core.utils.route.Route.formatParameters; | ||
4 | import static com.github.markozajc.akiwrapper.core.utils.route.Route.Endpoint.GAME_SERVER; | ||
5 | import static java.lang.System.currentTimeMillis; | ||
6 | |||
7 | import java.util.*; | ||
8 | import java.util.function.Supplier; | ||
9 | |||
10 | import javax.annotation.*; | ||
11 | |||
12 | import com.github.markozajc.akiwrapper.core.utils.route.Route.Endpoint; | ||
13 | |||
14 | @SuppressWarnings("javadoc") // internal util | ||
15 | public class RouteBuilder { | ||
16 | |||
17 | @Nonnull private final String path; | ||
18 | @Nonnull private Endpoint endpoint = GAME_SERVER; | ||
19 | private boolean requiresSession = false; | ||
20 | private boolean requiresFrontaddr = false; | ||
21 | private boolean requiresUidExtSession = false; | ||
22 | private boolean requiresUrlApiWs = false; | ||
23 | private Map<String, String> constantParameters; | ||
24 | private Map<String, Supplier<String>> automaticParameters; | ||
25 | private Map<String, String> profanityDisabledParameters; | ||
26 | private Map<String, String> profanityEnabledParameters; | ||
27 | private Set<String> mandatoryParameters; | ||
28 | private Map<String, String> parameters; // using Map instead of Set to avoid conversion later | ||
29 | |||
30 | public RouteBuilder(@Nonnull String path) { | ||
31 | this.path = path; | ||
32 | } | ||
33 | |||
34 | @Nonnull | ||
35 | public RouteBuilder endpoint(@Nonnull Endpoint endpoint) { | ||
36 | this.endpoint = endpoint; | ||
37 | return this; | ||
38 | } | ||
39 | |||
40 | @Nonnull | ||
41 | public RouteBuilder requiresSession() { | ||
42 | this.requiresSession = true; | ||
43 | return this; | ||
44 | } | ||
45 | |||
46 | @Nonnull | ||
47 | public RouteBuilder requiresFrontaddr() { | ||
48 | this.requiresFrontaddr = true; | ||
49 | return this; | ||
50 | } | ||
51 | |||
52 | @Nonnull | ||
53 | public RouteBuilder requiresUidExtSession() { | ||
54 | this.requiresUidExtSession = true; | ||
55 | return this; | ||
56 | } | ||
57 | |||
58 | @Nonnull | ||
59 | public RouteBuilder requiresUrlApiWs() { | ||
60 | this.requiresUrlApiWs = true; | ||
61 | return this; | ||
62 | } | ||
63 | |||
64 | @Nonnull | ||
65 | public RouteBuilder constantParameter(@Nonnull String key, @Nonnull String value) { | ||
66 | if (this.constantParameters == null) | ||
67 | this.constantParameters = new HashMap<>(); | ||
68 | this.constantParameters.put(key, value); | ||
69 | return this; | ||
70 | } | ||
71 | |||
72 | @Nonnull | ||
73 | public RouteBuilder automaticParameter(@Nonnull String key, @Nonnull Supplier<String> valueSupplier) { | ||
74 | if (this.automaticParameters == null) | ||
75 | this.automaticParameters = new HashMap<>(); | ||
76 | this.automaticParameters.put(key, valueSupplier); | ||
77 | return this; | ||
78 | } | ||
79 | |||
80 | @Nonnull | ||
81 | public RouteBuilder profanityDisabledParameter(@Nonnull String key, @Nonnull String value) { | ||
82 | if (this.profanityDisabledParameters == null) | ||
83 | this.profanityDisabledParameters = new HashMap<>(); | ||
84 | this.profanityDisabledParameters.put(key, value); | ||
85 | return this; | ||
86 | } | ||
87 | |||
88 | @Nonnull | ||
89 | public RouteBuilder profanityEnabledParameter(@Nonnull String key, @Nonnull String value) { | ||
90 | if (this.profanityEnabledParameters == null) | ||
91 | this.profanityEnabledParameters = new HashMap<>(); | ||
92 | this.profanityEnabledParameters.put(key, value); | ||
93 | return this; | ||
94 | } | ||
95 | |||
96 | @Nonnull | ||
97 | public RouteBuilder mandatoryParameter(@Nonnull String key) { | ||
98 | putParameter(key, null); | ||
99 | |||
100 | if (this.mandatoryParameters == null) | ||
101 | this.mandatoryParameters = new HashSet<>(); | ||
102 | this.mandatoryParameters.add(key); | ||
103 | |||
104 | return this; | ||
105 | } | ||
106 | |||
107 | @Nonnull | ||
108 | public RouteBuilder optionalParameter(@Nonnull String key) { | ||
109 | putParameter(key, null); | ||
110 | return this; | ||
111 | } | ||
112 | |||
113 | @Nonnull | ||
114 | public RouteBuilder defaultParameter(@Nonnull String key, @Nonnull String value) { | ||
115 | putParameter(key, value); | ||
116 | return this; | ||
117 | } | ||
118 | |||
119 | private void putParameter(@Nonnull String key, @Nullable String value) { | ||
120 | if (this.parameters == null) | ||
121 | this.parameters = new HashMap<>(); | ||
122 | |||
123 | this.parameters.put(key, value); | ||
124 | } | ||
125 | |||
126 | @SuppressWarnings("null") | ||
127 | public Route build() { | ||
128 | // these two are required by all routes (the callback one might only be required for | ||
129 | // NEW_SESSION, but it changes response parsing logic so I'm opting to just put it | ||
130 | // into all requests for future proofing if not anything else) | ||
131 | automaticParameter("_", () -> Long.toString(currentTimeMillis())); | ||
132 | constantParameter("callback", "jQuery"); | ||
133 | |||
134 | String processedPath = this.path; | ||
135 | if (this.constantParameters != null) | ||
136 | processedPath += "?" + formatParameters(this.constantParameters); | ||
137 | |||
138 | String profanityDisabledQuerystring = | ||
139 | this.profanityDisabledParameters == null ? null : formatParameters(this.profanityDisabledParameters); | ||
140 | |||
141 | String profanityEnabledQuerystring = | ||
142 | this.profanityDisabledParameters == null ? null : formatParameters(this.profanityEnabledParameters); | ||
143 | |||
144 | return new Route(processedPath, this.endpoint, this.requiresSession, this.requiresFrontaddr, | ||
145 | this.requiresUidExtSession, this.requiresUrlApiWs, this.automaticParameters, | ||
146 | profanityDisabledQuerystring, profanityEnabledQuerystring, this.mandatoryParameters, | ||
147 | this.parameters, this.constantParameters != null); | ||
148 | } | ||
149 | |||
150 | } | ||
diff --git a/src/main/java/com/github/markozajc/akiwrapper/core/utils/route/Routes.java b/src/main/java/com/github/markozajc/akiwrapper/core/utils/route/Routes.java new file mode 100644 index 0000000..33ed7b7 --- /dev/null +++ b/src/main/java/com/github/markozajc/akiwrapper/core/utils/route/Routes.java | |||
@@ -0,0 +1,154 @@ | |||
1 | package com.github.markozajc.akiwrapper.core.utils.route; | ||
2 | |||
3 | import static com.github.markozajc.akiwrapper.core.utils.route.Route.Endpoint.*; | ||
4 | |||
5 | @SuppressWarnings("javadoc") // internal util | ||
6 | public final class Routes { | ||
7 | |||
8 | /* | ||
9 | * Certain constant parameters are marked with "?" - this means that the parameter is | ||
10 | * set by the website (though not necessarily required), but I'm not sure of its | ||
11 | * function or significance. Feel free to open an issue if you know what any of them | ||
12 | * do so that I can note it down and possibly use them in the wrapper. | ||
13 | */ | ||
14 | |||
15 | private static final String PARAMETER_QUESTION_FILTER = "question_filter"; | ||
16 | |||
17 | private static final String VALUE_QUESTION_FILTER_PROFANITY_ENABLED = ""; | ||
18 | private static final String VALUE_QUESTION_FILTER_PROFANITY_DISABLED = "cat=1"; | ||
19 | |||
20 | public static final String PARAMETER_STEP = "step"; | ||
21 | public static final String PARAMETER_ANSWER = "answer"; | ||
22 | public static final String PARAMETER_SIZE = "size"; | ||
23 | public static final String PARAMETER_MAX_PIC_WIDTH = "max_pic_width"; | ||
24 | public static final String PARAMETER_MAX_PIC_HEIGHT = "max_pic_height"; | ||
25 | public static final String PARAMETER_ELEMENT = "element"; | ||
26 | |||
27 | /** | ||
28 | * Creates a new game session that all further state is associated with.<br> | ||
29 | * <i>This route takes no parameters.</i> | ||
30 | */ | ||
31 | public static final Route NEW_SESSION = new RouteBuilder("/new_session").endpoint(WEBSITE) | ||
32 | .requiresUrlApiWs() | ||
33 | .requiresFrontaddr() | ||
34 | .requiresUidExtSession() | ||
35 | .constantParameter("player", "website-desktop") // ? | ||
36 | .constantParameter("partner", "1") // ? | ||
37 | .constantParameter("constraint", "ETAT<>'AV'") // ? | ||
38 | .profanityEnabledParameter("childMod", "") | ||
39 | .profanityEnabledParameter("soft_constraint", "") | ||
40 | .profanityEnabledParameter(PARAMETER_QUESTION_FILTER, VALUE_QUESTION_FILTER_PROFANITY_ENABLED) | ||
41 | .profanityDisabledParameter("childMod", "true") | ||
42 | .profanityDisabledParameter("soft_constraint", "ETAT='EN'") | ||
43 | .profanityDisabledParameter(PARAMETER_QUESTION_FILTER, VALUE_QUESTION_FILTER_PROFANITY_DISABLED) | ||
44 | .build(); | ||
45 | |||
46 | /** | ||
47 | * Answers the current question and fetches the next one. <br> | ||
48 | * Parameters: | ||
49 | * <ul> | ||
50 | * <li>{@link Routes#PARAMETER_STEP}: must always point to the current zero-index | ||
51 | * step</li> | ||
52 | * <li>{@link Routes#PARAMETER_ANSWER}: the answer to the current question</li> | ||
53 | * </ul> | ||
54 | * <b>This route requires a session</b> | ||
55 | */ | ||
56 | public static final Route ANSWER = new RouteBuilder("/answer_api").endpoint(WEBSITE) | ||
57 | .requiresUrlApiWs() | ||
58 | .requiresFrontaddr() | ||
59 | .requiresSession() | ||
60 | .profanityEnabledParameter(PARAMETER_QUESTION_FILTER, VALUE_QUESTION_FILTER_PROFANITY_ENABLED) | ||
61 | .profanityDisabledParameter(PARAMETER_QUESTION_FILTER, VALUE_QUESTION_FILTER_PROFANITY_DISABLED) | ||
62 | .mandatoryParameter(PARAMETER_STEP) | ||
63 | .mandatoryParameter(PARAMETER_ANSWER) | ||
64 | .build(); | ||
65 | |||
66 | /** | ||
67 | * Cancels (undoes) an answer and fetches the previous question. Parameters: | ||
68 | * <ul> | ||
69 | * <li>{@link Routes#PARAMETER_STEP}: must always point to the current zero-index | ||
70 | * step</li> | ||
71 | * </ul> | ||
72 | * <b>This route requires a session</b> | ||
73 | */ | ||
74 | public static final Route CANCEL_ANSWER = new RouteBuilder("/cancel_answer").endpoint(GAME_SERVER) | ||
75 | .requiresSession() | ||
76 | .constantParameter(PARAMETER_ANSWER, "-1") | ||
77 | .profanityEnabledParameter(PARAMETER_QUESTION_FILTER, VALUE_QUESTION_FILTER_PROFANITY_ENABLED) | ||
78 | .profanityDisabledParameter(PARAMETER_QUESTION_FILTER, VALUE_QUESTION_FILTER_PROFANITY_DISABLED) | ||
79 | .mandatoryParameter(PARAMETER_STEP) | ||
80 | .build(); | ||
81 | |||
82 | /** | ||
83 | * Lists available guesses. Parameters: | ||
84 | * <ul> | ||
85 | * <li>{@link Routes#PARAMETER_STEP}: must always point to the current zero-index | ||
86 | * step</li> | ||
87 | * <li><i>(optional)</i> {@link Routes#PARAMETER_SIZE}: the number of guesses to | ||
88 | * fetch (or all if not set)</li> | ||
89 | * <li><i>(defaults to 246)</i> {@link Routes#PARAMETER_MAX_PIC_WIDTH}: presumably | ||
90 | * the max width of the returned image, but I haven't checked if that's actually | ||
91 | * true</li> | ||
92 | * <li><i>(defaults to 294)</i> {@link Routes#PARAMETER_MAX_PIC_HEIGHT}: ditto for | ||
93 | * height</li> | ||
94 | * </ul> | ||
95 | * <b>This route requires a session</b> | ||
96 | */ | ||
97 | public static final Route LIST = new RouteBuilder("/list").endpoint(GAME_SERVER) | ||
98 | .requiresSession() | ||
99 | .constantParameter("pref_photos", "VO-OK") // ? | ||
100 | .constantParameter("duel_allowed", "1") // ? | ||
101 | .constantParameter("mode_question", "0") // ? | ||
102 | .defaultParameter(PARAMETER_MAX_PIC_HEIGHT, "294") | ||
103 | .defaultParameter(PARAMETER_MAX_PIC_WIDTH, "246") | ||
104 | .mandatoryParameter(PARAMETER_STEP) | ||
105 | .optionalParameter(PARAMETER_SIZE) | ||
106 | .build(); | ||
107 | |||
108 | /** | ||
109 | * <b>IMPORTANT: This route is EXCLUDED from tests!</b> Because automated tests don't | ||
110 | * tend to behave like players, calling this during testing might introduce faulty | ||
111 | * data into Akinator's algorithm, so please avoid doing that.<br> | ||
112 | * Exclude a guess. Apparently this won't prevent it from showing up in {@link #LIST} | ||
113 | * for whatever reason, but it might improve the further questions. Parameters: | ||
114 | * <ul> | ||
115 | * <li>{@link Routes#PARAMETER_STEP}: must always point to the current zero-index | ||
116 | * step</li> | ||
117 | * </ul> | ||
118 | * <b>This route requires a session</b> | ||
119 | * | ||
120 | * @apiNote Considering that it doesn't take a question ID parameter and that it's | ||
121 | * called right after a guess is rejected on the website, I can only assume | ||
122 | * that this excludes the top guess. | ||
123 | */ | ||
124 | public static final Route EXCLUSION = new RouteBuilder("/exclusion").endpoint(GAME_SERVER) | ||
125 | .requiresSession() | ||
126 | .constantParameter("forward_answer", "1") // ? | ||
127 | .mandatoryParameter(PARAMETER_STEP) | ||
128 | .build(); | ||
129 | |||
130 | /** | ||
131 | * <b>IMPORTANT: This route is EXCLUDED from tests!</b> Because automated tests don't | ||
132 | * tend to behave like players, calling this during testing might introduce faulty | ||
133 | * data into Akinator's algorithm, so please avoid doing that.<br> | ||
134 | * Confirm a guess. While this doesn't affect the current session, because it's | ||
135 | * called at the very end, it likely affects Akinator's algorithm and associates the | ||
136 | * taken answer route with the confirmed guess, thus improving the game for everyone. | ||
137 | * Parameters: | ||
138 | * <ul> | ||
139 | * <li>{@link Routes#PARAMETER_STEP}: must always point to the current zero-index | ||
140 | * step</li> | ||
141 | * <li>{@link Routes#PARAMETER_ELEMENT}: the guess ID to confirm</li> | ||
142 | * </ul> | ||
143 | * <b>This route requires a session</b> | ||
144 | */ | ||
145 | public static final Route CHOICE = new RouteBuilder("/choice").endpoint(GAME_SERVER) | ||
146 | .requiresSession() | ||
147 | .constantParameter("duel_allowed", "1") // ? | ||
148 | .mandatoryParameter(PARAMETER_STEP) | ||
149 | .mandatoryParameter(PARAMETER_ELEMENT) | ||
150 | .build(); | ||
151 | |||
152 | private Routes() {} | ||
153 | |||
154 | } | ||
diff --git a/src/test/java/com/github/markozajc/akiwrapper/IntegrationTest.java b/src/test/java/com/github/markozajc/akiwrapper/IntegrationTest.java index 0bd9ec9..476a20d 100644 --- a/src/test/java/com/github/markozajc/akiwrapper/IntegrationTest.java +++ b/src/test/java/com/github/markozajc/akiwrapper/IntegrationTest.java | |||
@@ -1,5 +1,6 @@ | |||
1 | package com.github.markozajc.akiwrapper; | 1 | package com.github.markozajc.akiwrapper; |
2 | 2 | ||
3 | import static com.github.markozajc.akiwrapper.Akiwrapper.Answer.YES; | ||
3 | import static java.lang.String.format; | 4 | import static java.lang.String.format; |
4 | import static org.junit.jupiter.api.Assumptions.abort; | 5 | import static org.junit.jupiter.api.Assumptions.abort; |
5 | import static org.slf4j.LoggerFactory.getLogger; | 6 | import static org.slf4j.LoggerFactory.getLogger; |
@@ -11,17 +12,19 @@ import javax.annotation.*; | |||
11 | 12 | ||
12 | import org.junit.jupiter.params.ParameterizedTest; | 13 | import org.junit.jupiter.params.ParameterizedTest; |
13 | import org.junit.jupiter.params.provider.*; | 14 | import org.junit.jupiter.params.provider.*; |
15 | import org.opentest4j.TestAbortedException; | ||
14 | import org.slf4j.Logger; | 16 | import org.slf4j.Logger; |
15 | 17 | ||
16 | import com.github.markozajc.akiwrapper.Akiwrapper.Answer; | 18 | import com.github.markozajc.akiwrapper.Akiwrapper.Answer; |
17 | import com.github.markozajc.akiwrapper.core.entities.*; | 19 | import com.github.markozajc.akiwrapper.core.entities.*; |
18 | import com.github.markozajc.akiwrapper.core.entities.Server.*; | 20 | import com.github.markozajc.akiwrapper.core.entities.Server.*; |
19 | import com.github.markozajc.akiwrapper.core.exceptions.ServerNotFoundException; | 21 | import com.github.markozajc.akiwrapper.core.exceptions.*; |
20 | 22 | ||
21 | import static org.junit.jupiter.api.Assertions.*; | 23 | import static org.junit.jupiter.api.Assertions.*; |
22 | 24 | ||
23 | class IntegrationTest { | 25 | class IntegrationTest { |
24 | 26 | ||
27 | private static final String FETCHING_GUESSES_THROWS = "Fetching guesses throws"; | ||
25 | private static final String SERVER_GUESSTYPE_NO_MATCH = | 28 | private static final String SERVER_GUESSTYPE_NO_MATCH = |
26 | "The wanted and actual guess type of the server don't match."; | 29 | "The wanted and actual guess type of the server don't match."; |
27 | private static final String SERVER_LANGUAGE_NO_MATCH = "The wanted and actual language 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."; |
@@ -36,65 +39,139 @@ class IntegrationTest { | |||
36 | @ParameterizedTest | 39 | @ParameterizedTest |
37 | @MethodSource("generateTestAkiwrapper") | 40 | @MethodSource("generateTestAkiwrapper") |
38 | void testAkiwrapper(@Nonnull Language language, @Nonnull GuessType guessType) { | 41 | void testAkiwrapper(@Nonnull Language language, @Nonnull GuessType guessType) { |
39 | Logger log = getLogger(format("%s-%s", language, guessType)); | ||
40 | log.info("Establishing connection"); | ||
41 | Akiwrapper api; | ||
42 | try { | 42 | try { |
43 | api = new AkiwrapperBuilder().setLanguage(language).setGuessType(guessType).build(); | 43 | Logger log = getLogger(format("%s-%s", language, guessType)); |
44 | } catch (ServerNotFoundException e) { | 44 | log.info("Establishing connection"); |
45 | abort("Current combination not supported, server wasn't found."); | 45 | Akiwrapper api; |
46 | return; | 46 | try { |
47 | api = new AkiwrapperBuilder().setLanguage(language).setGuessType(guessType).build(); | ||
48 | } catch (ServerNotFoundException e) { | ||
49 | abort("Current combination not supported, server wasn't found."); | ||
50 | return; | ||
51 | } | ||
52 | |||
53 | Question initialQuestion = api.getQuestion(); | ||
54 | testInitialState(log, api, initialQuestion, language, guessType); | ||
55 | int expectedState = testAnswering(log, api); | ||
56 | testUndo(log, api, initialQuestion, expectedState); | ||
57 | testExhaustion(log, api); | ||
58 | } catch (TestAbortedException e) { | ||
59 | throw e; | ||
60 | |||
61 | } catch (Exception e) { | ||
62 | e.printStackTrace(); | ||
63 | fail("Got an exception running the test"); | ||
47 | } | 64 | } |
65 | } | ||
48 | 66 | ||
67 | private static void testInitialState(@Nonnull Logger log, @Nonnull Akiwrapper api, | ||
68 | @Nullable Question initialQuestion, @Nonnull Language language, | ||
69 | @Nonnull GuessType guessType) { | ||
49 | log.info("Asserting the current state."); | 70 | log.info("Asserting the current state."); |
50 | Question initialQuestion = api.getQuestion(); | 71 | checkQuestion(0, initialQuestion); |
51 | int expectedState = 0; | 72 | assertDoesNotThrow(() -> api.getGuesses(), FETCHING_GUESSES_THROWS); |
52 | checkQuestion(initialQuestion, expectedState); | ||
53 | assertEquals(language, api.getServer().getLanguage(), SERVER_LANGUAGE_NO_MATCH); | 73 | assertEquals(language, api.getServer().getLanguage(), SERVER_LANGUAGE_NO_MATCH); |
54 | assertEquals(guessType, api.getServer().getGuessType(), SERVER_GUESSTYPE_NO_MATCH); | 74 | assertEquals(guessType, api.getServer().getGuessType(), SERVER_GUESSTYPE_NO_MATCH); |
55 | log.trace("API server URL: {}", api.getServer().getUrl()); | 75 | log.trace("API server URL: {}", api.getServer().getUrl()); |
76 | } | ||
56 | 77 | ||
78 | private static int testAnswering(@Nonnull Logger log, @Nonnull Akiwrapper api) { | ||
57 | log.info("Advancing {} steps (one time for each possible answer).", Answer.values().length); | 79 | log.info("Advancing {} steps (one time for each possible answer).", Answer.values().length); |
80 | int expectedState = 0; | ||
58 | for (Answer answer : Answer.values()) { | 81 | for (Answer answer : Answer.values()) { |
59 | log.debug("Answering with {} and checking the question.", answer.name()); | 82 | log.info("Answering with {} and checking the state (step={}).", answer.name(), api.getStep()); |
60 | Question newQuestion = api.answer(answer); | 83 | Question newQuestion = api.answer(answer); |
61 | assertEquals(newQuestion, api.getQuestion(), QUESTION_CURRENT_NO_MATCH); | 84 | assertEquals(newQuestion, api.getQuestion(), QUESTION_CURRENT_NO_MATCH); |
62 | expectedState++; | 85 | expectedState++; |
63 | checkQuestion(api.getQuestion(), expectedState); | 86 | assertEquals(expectedState, api.getStep()); |
87 | checkQuestion(expectedState, api.getQuestion()); | ||
88 | checkGuessCount(log, api); | ||
64 | } | 89 | } |
65 | 90 | ||
66 | log.info("Fetching guesses.", Answer.values().length); | 91 | log.info("Asserting the current state."); |
67 | List<Guess> guesses = api.getGuesses(); | 92 | fetchAndDebugGuesses(log, api); |
68 | debugGuesses(log, guesses); | 93 | return expectedState; |
94 | } | ||
69 | 95 | ||
96 | private static void testUndo(@Nonnull Logger log, @Nonnull Akiwrapper api, @Nullable Question initialQuestion, | ||
97 | int initialExpectedState) { | ||
70 | log.info("Advancing -{} steps (using undo).", Answer.values().length); | 98 | log.info("Advancing -{} steps (using undo).", Answer.values().length); |
99 | int expectedState = initialExpectedState; | ||
71 | for (int i = 0; i < Answer.values().length; i++) { | 100 | for (int i = 0; i < Answer.values().length; i++) { |
101 | log.info("Undoing a step and checking the state (step={}).", api.getStep()); | ||
72 | Question undoneQuestion = api.undoAnswer(); | 102 | Question undoneQuestion = api.undoAnswer(); |
73 | assertEquals(undoneQuestion, api.getQuestion(), QUESTION_CURRENT_NO_MATCH); | 103 | assertEquals(undoneQuestion, api.getQuestion(), QUESTION_CURRENT_NO_MATCH); |
74 | expectedState--; | 104 | expectedState--; |
75 | checkQuestion(api.getQuestion(), expectedState); | 105 | assertEquals(expectedState, api.getStep()); |
106 | checkQuestion(expectedState, api.getQuestion()); | ||
107 | checkGuessCount(log, api); | ||
76 | } | 108 | } |
77 | 109 | ||
78 | log.info("Asserting the final state."); | 110 | log.info("Asserting the current state."); |
79 | assertNull(api.undoAnswer()); | 111 | assertThrows(UndoOutOfBoundsException.class, () -> api.undoAnswer()); |
80 | checkQuestion(api.getQuestion(), 0); | 112 | checkQuestion(0, api.getQuestion()); |
113 | assertDoesNotThrow(() -> api.getGuesses(), FETCHING_GUESSES_THROWS); | ||
81 | Question currentQuestion = api.getQuestion(); | 114 | Question currentQuestion = api.getQuestion(); |
82 | if (initialQuestion != null && currentQuestion != null) { | 115 | if (initialQuestion != null && currentQuestion != null) |
83 | assertEquals(initialQuestion.getQuestion(), currentQuestion.getQuestion(), QUESTION_INITIAL_NO_MATCH); | 116 | assertEquals(initialQuestion.getQuestion(), currentQuestion.getQuestion(), QUESTION_INITIAL_NO_MATCH); |
117 | else | ||
118 | fail("initialQuestion or currentQuestion were somehow null"); | ||
119 | // using this syntax instead of assertNotNull to please null analysis of @Nullable | ||
120 | } | ||
121 | |||
122 | private static void testExhaustion(@Nonnull Logger log, @Nonnull Akiwrapper api) { | ||
123 | log.info("Exhausting questions by answering YES to all."); | ||
124 | var lastQuestion = api.getQuestion(); | ||
125 | assertNotNull(lastQuestion, "Current question is already null"); | ||
126 | |||
127 | int i = api.getStep(); | ||
128 | while (true) { | ||
129 | checkQuestion(i, api.getQuestion()); | ||
130 | assertEquals(i, api.getStep()); | ||
131 | |||
132 | var question = api.answer(YES); | ||
133 | if (question == null) { | ||
134 | log.info("Ran out at step {}.", api.getStep()); | ||
135 | break; | ||
136 | |||
137 | } else { | ||
138 | log.info("Exhausting questions (step={})", api.getStep()); | ||
139 | checkQuestion(++i, question); | ||
140 | } | ||
141 | |||
142 | if (i > 80) | ||
143 | fail("Got over step 80, API must have changed. Ensure there are no side effects and find the new limit."); | ||
84 | } | 144 | } |
85 | // Neither of those can be null due to checkQuestion checking (and failing on) | ||
86 | // nullability. | ||
87 | 145 | ||
146 | log.info("Asserting the current state."); | ||
147 | assertThrows(QuestionsExhaustedException.class, () -> api.answer(YES)); | ||
148 | assertThrows(QuestionsExhaustedException.class, () -> api.undoAnswer()); | ||
149 | assertDoesNotThrow(() -> api.getGuesses()); | ||
150 | |||
151 | fetchAndDebugGuesses(log, api); | ||
152 | } | ||
153 | |||
154 | private static void checkGuessCount(Logger log, Akiwrapper api) { | ||
155 | for (int i = 1; i < 5; i++) { | ||
156 | log.info("Fetching {} guesses.", i); | ||
157 | assertTrue(api.getGuesses(i).size() <= i, "Got more guesses than requested from the API"); | ||
158 | } | ||
159 | } | ||
160 | |||
161 | private static void fetchAndDebugGuesses(Logger log, Akiwrapper api) { | ||
162 | log.info("Fetching all guesses.", Answer.values().length); | ||
163 | List<Guess> guesses = api.getGuesses(); | ||
164 | debugGuesses(log, guesses); | ||
88 | } | 165 | } |
89 | 166 | ||
90 | private static void debugGuesses(Logger log, List<Guess> guesses) { | 167 | private static void debugGuesses(Logger log, List<Guess> guesses) { |
91 | log.debug("There are {} guesses.", guesses.size()); | 168 | log.info("There are {} guesses.", guesses.size()); |
92 | for (Guess guess : guesses) { | 169 | for (Guess guess : guesses) { |
93 | log.trace("{} - {}", guess.getProbability(), guess.getName()); | 170 | log.info("{} - {}", guess.getProbability(), guess.getName()); |
94 | } | 171 | } |
95 | } | 172 | } |
96 | 173 | ||
97 | private static void checkQuestion(@Nullable Question question, int expectedState) { | 174 | private static void checkQuestion(int expectedState, @Nullable Question question) { |
98 | if (question == null) { | 175 | if (question == null) { |
99 | fail(QUESTION_NULL); | 176 | fail(QUESTION_NULL); |
100 | } else { | 177 | } else { |
diff --git a/src/test/java/com/github/markozajc/akiwrapper/core/RouteTest.java b/src/test/java/com/github/markozajc/akiwrapper/core/RouteTest.java deleted file mode 100644 index 79b214c..0000000 --- a/src/test/java/com/github/markozajc/akiwrapper/core/RouteTest.java +++ /dev/null | |||
@@ -1,43 +0,0 @@ | |||
1 | package com.github.markozajc.akiwrapper.core; | ||
2 | |||
3 | import static kong.unirest.Unirest.spawnInstance; | ||
4 | |||
5 | import org.json.JSONObject; | ||
6 | import org.junit.jupiter.api.Test; | ||
7 | |||
8 | import com.github.markozajc.akiwrapper.core.exceptions.*; | ||
9 | |||
10 | import static org.junit.jupiter.api.Assertions.*; | ||
11 | |||
12 | class RouteTest { | ||
13 | |||
14 | @Test | ||
15 | @SuppressWarnings("null") | ||
16 | void testMissingParameters() { | ||
17 | try (var unirest = spawnInstance()) { | ||
18 | assertThrows(IllegalArgumentException.class, | ||
19 | () -> Route.NEW_SESSION.createRequest(unirest, "", false /* and no parameters */)); | ||
20 | } | ||
21 | } | ||
22 | |||
23 | @Test | ||
24 | void testTestResponse() { | ||
25 | JSONObject base = new JSONObject(); | ||
26 | |||
27 | base.put("completion", "KO - SERVER DOWN"); | ||
28 | assertThrows(ServerUnavailableException.class, () -> Route.Request.ensureSuccessful(base)); | ||
29 | |||
30 | base.put("completion", "KO - TEST REASON"); | ||
31 | assertThrows(StatusException.class, () -> Route.Request.ensureSuccessful(base)); | ||
32 | |||
33 | base.put("completion", "WARN - TEST REASON"); | ||
34 | assertDoesNotThrow(() -> Route.Request.ensureSuccessful(base)); | ||
35 | |||
36 | base.put("completion", "OK - TEST REASON"); | ||
37 | assertDoesNotThrow(() -> Route.Request.ensureSuccessful(base)); | ||
38 | |||
39 | base.put("completion", "OK"); | ||
40 | assertDoesNotThrow(() -> Route.Request.ensureSuccessful(base)); | ||
41 | } | ||
42 | |||
43 | } | ||
diff --git a/src/test/java/com/github/markozajc/akiwrapper/core/entities/IdentifiableTest.java b/src/test/java/com/github/markozajc/akiwrapper/core/entities/IdentifiableTest.java index 821f7a2..6996a58 100644 --- a/src/test/java/com/github/markozajc/akiwrapper/core/entities/IdentifiableTest.java +++ b/src/test/java/com/github/markozajc/akiwrapper/core/entities/IdentifiableTest.java | |||
@@ -13,13 +13,9 @@ class IdentifiableTest { | |||
13 | } | 13 | } |
14 | 14 | ||
15 | @Test | 15 | @Test |
16 | @SuppressWarnings("null") | ||
16 | void testGetIdLongLong() { | 17 | void testGetIdLongLong() { |
17 | String maxLongString = Long.toString(Long.MAX_VALUE); | 18 | String maxLongString = Long.toString(Long.MAX_VALUE); |
18 | if (maxLongString == null) { | ||
19 | fail(); // Sorry suppress warnings broke and would let me ignore null warnings | ||
20 | return; // Also because eclipse doesn't realize that fail throws | ||
21 | } | ||
22 | |||
23 | Identifiable identifiable = () -> maxLongString; | 19 | Identifiable identifiable = () -> maxLongString; |
24 | assertEquals(Long.MAX_VALUE, identifiable.getIdLong()); | 20 | assertEquals(Long.MAX_VALUE, identifiable.getIdLong()); |
25 | } | 21 | } |
diff --git a/src/test/java/com/github/markozajc/akiwrapper/core/entities/impl/immutable/ServerListImplTest.java b/src/test/java/com/github/markozajc/akiwrapper/core/entities/impl/immutable/ServerListImplTest.java deleted file mode 100644 index f67c586..0000000 --- a/src/test/java/com/github/markozajc/akiwrapper/core/entities/impl/immutable/ServerListImplTest.java +++ /dev/null | |||
@@ -1,67 +0,0 @@ | |||
1 | package com.github.markozajc.akiwrapper.core.entities.impl.immutable; | ||
2 | |||
3 | import static com.github.markozajc.akiwrapper.core.entities.Server.GuessType.ANIMAL; | ||
4 | import static com.github.markozajc.akiwrapper.core.entities.Server.Language.*; | ||
5 | import static java.util.Arrays.asList; | ||
6 | |||
7 | import java.util.*; | ||
8 | |||
9 | import org.junit.jupiter.api.Test; | ||
10 | |||
11 | import com.github.markozajc.akiwrapper.core.entities.*; | ||
12 | |||
13 | import static org.junit.jupiter.api.Assertions.*; | ||
14 | |||
15 | class ServerListImplTest { | ||
16 | |||
17 | @SuppressWarnings("null") | ||
18 | @Test | ||
19 | void testEmptyCollection() { | ||
20 | List<Server> emptyList = Collections.emptyList(); | ||
21 | assertThrows(IllegalArgumentException.class, () -> new ServerListImpl(emptyList)); | ||
22 | } | ||
23 | |||
24 | @SuppressWarnings("null") | ||
25 | @Test | ||
26 | void testServersCollection() { | ||
27 | List<Server> serversList = asList(new ServerImpl("x", ARABIC, ANIMAL), new ServerImpl("x", FRENCH, ANIMAL)); | ||
28 | ServerList serverList = new ServerListImpl(serversList); | ||
29 | assertEquals(serversList.size() - 1, serverList.getRemainingSize()); | ||
30 | assertEquals(serversList, serverList.getServers()); | ||
31 | assertEquals(ARABIC, serverList.getLanguage()); | ||
32 | assertTrue(serverList.hasNext()); | ||
33 | assertTrue(serverList.next()); | ||
34 | assertEquals(FRENCH, serverList.getLanguage()); | ||
35 | assertFalse(serverList.hasNext()); | ||
36 | assertFalse(serverList.next()); | ||
37 | } | ||
38 | |||
39 | @SuppressWarnings("null") | ||
40 | @Test | ||
41 | void testNestedServersCollection() { | ||
42 | List<Server> serversList = asList(new ServerImpl("x", ARABIC, ANIMAL), new ServerImpl("x", FRENCH, ANIMAL)); | ||
43 | ServerList serverList = new ServerListImpl(Arrays.asList(new ServerListImpl(serversList))); | ||
44 | assertEquals(serversList.size() - 1, serverList.getRemainingSize()); | ||
45 | assertEquals(serversList, serverList.getServers()); | ||
46 | assertEquals(ARABIC, serverList.getLanguage()); | ||
47 | assertTrue(serverList.hasNext()); | ||
48 | assertTrue(serverList.next()); | ||
49 | assertEquals(FRENCH, serverList.getLanguage()); | ||
50 | assertFalse(serverList.hasNext()); | ||
51 | assertFalse(serverList.next()); | ||
52 | } | ||
53 | |||
54 | @Test | ||
55 | void testMixedServersCollection() { | ||
56 | ServerList serverList = new ServerListImpl(new ServerImpl("x", ARABIC, ANIMAL), | ||
57 | new ServerListImpl(new ServerImpl("x", FRENCH, ANIMAL))); | ||
58 | assertEquals(2 /* amount of servers */ - 1, serverList.getRemainingSize()); | ||
59 | assertEquals(ARABIC, serverList.getLanguage()); | ||
60 | assertTrue(serverList.hasNext()); | ||
61 | assertTrue(serverList.next()); | ||
62 | assertEquals(FRENCH, serverList.getLanguage()); | ||
63 | assertFalse(serverList.hasNext()); | ||
64 | assertFalse(serverList.next()); | ||
65 | } | ||
66 | |||
67 | } | ||
diff --git a/src/test/java/com/github/markozajc/akiwrapper/core/entities/impl/immutable/StatusImplTest.java b/src/test/java/com/github/markozajc/akiwrapper/core/entities/impl/immutable/StatusImplTest.java index 9889ec3..ef90421 100644 --- a/src/test/java/com/github/markozajc/akiwrapper/core/entities/impl/immutable/StatusImplTest.java +++ b/src/test/java/com/github/markozajc/akiwrapper/core/entities/impl/immutable/StatusImplTest.java | |||
@@ -1,46 +1,62 @@ | |||
1 | package com.github.markozajc.akiwrapper.core.entities.impl.immutable; | 1 | package com.github.markozajc.akiwrapper.core.entities.impl.immutable; |
2 | 2 | ||
3 | import static com.github.markozajc.akiwrapper.core.entities.Status.Reason.*; | ||
3 | import static org.junit.jupiter.params.provider.EnumSource.Mode.EXCLUDE; | 4 | import static org.junit.jupiter.params.provider.EnumSource.Mode.EXCLUDE; |
4 | 5 | ||
5 | import java.util.stream.Stream; | 6 | import java.util.stream.Stream; |
6 | 7 | ||
7 | import javax.annotation.Nonnull; | 8 | import javax.annotation.Nonnull; |
8 | 9 | ||
10 | import org.junit.jupiter.api.Test; | ||
9 | import org.junit.jupiter.params.ParameterizedTest; | 11 | import org.junit.jupiter.params.ParameterizedTest; |
10 | import org.junit.jupiter.params.provider.*; | 12 | import org.junit.jupiter.params.provider.*; |
11 | 13 | ||
12 | import com.github.markozajc.akiwrapper.core.entities.Status; | ||
13 | import com.github.markozajc.akiwrapper.core.entities.Status.Level; | 14 | import com.github.markozajc.akiwrapper.core.entities.Status.Level; |
15 | import com.github.markozajc.akiwrapper.core.entities.impl.StatusImpl; | ||
14 | 16 | ||
15 | import static org.junit.jupiter.api.Assertions.*; | 17 | import static org.junit.jupiter.api.Assertions.*; |
16 | 18 | ||
17 | class StatusImplTest { | 19 | class StatusImplTest { |
18 | 20 | ||
21 | @Test | ||
22 | void testReason() { | ||
23 | assertEquals(OK, StatusImpl.fromCompletion("OK").getReason()); | ||
24 | assertEquals(QUESTIONS_EXHAUSTED, StatusImpl.fromCompletion("WARN - NO QUESTION").getReason()); | ||
25 | assertEquals(SERVER_FAILURE, StatusImpl.fromCompletion("KO - TECHNICAL ERROR").getReason()); | ||
26 | assertEquals(LIBRARY_FAILURE, StatusImpl.fromCompletion("KO - UNAUTHORIZED").getReason()); | ||
27 | assertEquals(LIBRARY_FAILURE, StatusImpl.fromCompletion("KO - ELEM LIST IS EMPTY").getReason()); | ||
28 | assertEquals(LIBRARY_FAILURE, StatusImpl.fromCompletion("KO - MISSING KEY").getReason()); | ||
29 | assertEquals(LIBRARY_FAILURE, StatusImpl.fromCompletion("KO - MISSING PARAMETERS").getReason()); | ||
30 | assertEquals(UNKNOWN, StatusImpl.fromCompletion("OK - MESSAGE").getReason()); | ||
31 | assertEquals(UNKNOWN, StatusImpl.fromCompletion("WARN - MESSAGE").getReason()); | ||
32 | assertEquals(UNKNOWN, StatusImpl.fromCompletion("KO - MESSAGE").getReason()); | ||
33 | } | ||
34 | |||
19 | @ParameterizedTest | 35 | @ParameterizedTest |
20 | @EnumSource(value = Level.class, mode = EXCLUDE) | 36 | @EnumSource(value = Level.class, mode = EXCLUDE) |
21 | void testStringConstructorNoReason(@Nonnull Level level) { | 37 | void testStringConstructorNoMessage(@Nonnull Level level) { |
22 | @SuppressWarnings("null") | 38 | @SuppressWarnings("null") |
23 | Status status = new StatusImpl(level.toString()); | 39 | var status = StatusImpl.fromCompletion(level.toString()); |
24 | assertEquals(level, status.getLevel()); | 40 | assertEquals(level, status.getLevel()); |
25 | assertNull(status.getReason()); | 41 | assertNull(status.getMessage()); |
26 | } | 42 | } |
27 | 43 | ||
28 | @ParameterizedTest | 44 | @ParameterizedTest |
29 | @MethodSource("generateTestStringConstructorWithReason") | 45 | @MethodSource("generateTestStringConstructorWithMessage") |
30 | void testStringConstructorWithReason(@Nonnull Level level, @Nonnull String reason) { | 46 | void testStringConstructorWithMessage(@Nonnull Level level, @Nonnull String message) { |
31 | String completion = level.toString() + " - " + reason; | 47 | String completion = level.toString() + " - " + message; |
32 | Status status = new StatusImpl(completion); | 48 | var status = StatusImpl.fromCompletion(completion); |
33 | assertEquals(level, status.getLevel()); | 49 | assertEquals(level, status.getLevel()); |
34 | assertEquals(reason, status.getReason()); | 50 | assertEquals(message, status.getMessage()); |
35 | } | 51 | } |
36 | 52 | ||
37 | private static Stream<Arguments> generateTestStringConstructorWithReason() { | 53 | private static Stream<Arguments> generateTestStringConstructorWithMessage() { |
38 | String[] reasons = { "", "reason", "reason with spaces", "UPPERCASE", "UPPERCASE WITH SPACES" }; | 54 | String[] messages = { "", "message", "message with spaces", "UPPERCASE", "UPPERCASE WITH SPACES" }; |
39 | Arguments[] arguments = new Arguments[Level.values().length * reasons.length]; | 55 | Arguments[] arguments = new Arguments[Level.values().length * messages.length]; |
40 | int i = 0; | 56 | int i = 0; |
41 | for (Level level : Level.values()) | 57 | for (Level level : Level.values()) |
42 | for (String reason : reasons) { | 58 | for (String message : messages) { |
43 | arguments[i] = Arguments.of(level, reason); | 59 | arguments[i] = Arguments.of(level, message); |
44 | i++; | 60 | i++; |
45 | } | 61 | } |
46 | return Stream.of(arguments); | 62 | return Stream.of(arguments); |