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