diff --git a/pom.xml b/pom.xml
index b8ef856..6ff77ab 100644
--- a/pom.xml
+++ b/pom.xml
@@ -41,6 +41,22 @@
spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-data-redis
+
+
+ io.lettuce
+ lettuce-core
+
+
+
+
+ redis.clients
+ jedis
+
+
org.projectlombok
diff --git a/src/main/java/me/braydon/mc/RESTfulMC.java b/src/main/java/me/braydon/mc/RESTfulMC.java
index 9e61dbf..5d9ea44 100644
--- a/src/main/java/me/braydon/mc/RESTfulMC.java
+++ b/src/main/java/me/braydon/mc/RESTfulMC.java
@@ -5,8 +5,13 @@ import com.google.gson.GsonBuilder;
import lombok.NonNull;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
+import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
+import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
import java.io.File;
import java.nio.file.Files;
@@ -23,6 +28,30 @@ public class RESTfulMC {
.serializeNulls()
.create();
+ /**
+ * The Redis server host.
+ */
+ @Value("${spring.data.redis.host}")
+ private String redisHost;
+
+ /**
+ * The Redis server port.
+ */
+ @Value("${spring.data.redis.port}")
+ private int redisPort;
+
+ /**
+ * The Redis database index.
+ */
+ @Value("${spring.data.redis.database}")
+ private int redisDatabase;
+
+ /**
+ * The optional Redis password.
+ */
+ @Value("${spring.data.redis.auth}")
+ private String redisAuth;
+
@SneakyThrows
public static void main(@NonNull String[] args) {
// Handle loading of our configuration file
@@ -39,4 +68,34 @@ public class RESTfulMC {
// Start the app
SpringApplication.run(RESTfulMC.class, args);
}
+
+ /**
+ * Build the config to use for Redis.
+ *
+ * @return the config
+ * @see RedisTemplate for config
+ */
+ @Bean @NonNull
+ public RedisTemplate redisTemplate() {
+ RedisTemplate template = new RedisTemplate<>();
+ template.setConnectionFactory(jedisConnectionFactory());
+ return template;
+ }
+
+ /**
+ * Build the connection factory to use
+ * when making connections to Redis.
+ *
+ * @return the built factory
+ * @see JedisConnectionFactory for factory
+ */
+ @Bean @NonNull
+ public JedisConnectionFactory jedisConnectionFactory() {
+ RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(redisHost, redisPort);
+ config.setDatabase(redisDatabase);
+ if (!redisAuth.trim().isEmpty()) { // Auth with our provided password
+ config.setPassword(redisAuth);
+ }
+ return new JedisConnectionFactory(config);
+ }
}
\ No newline at end of file
diff --git a/src/main/java/me/braydon/mc/controller/PlayerController.java b/src/main/java/me/braydon/mc/controller/PlayerController.java
index 8a4ffb1..2ab7cf4 100644
--- a/src/main/java/me/braydon/mc/controller/PlayerController.java
+++ b/src/main/java/me/braydon/mc/controller/PlayerController.java
@@ -4,7 +4,7 @@ import lombok.NonNull;
import lombok.extern.log4j.Log4j2;
import me.braydon.mc.exception.impl.BadRequestException;
import me.braydon.mc.exception.impl.ResourceNotFoundException;
-import me.braydon.mc.model.Player;
+import me.braydon.mc.model.cache.CachedPlayer;
import me.braydon.mc.service.MojangService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
@@ -41,7 +41,7 @@ public final class PlayerController {
*/
@GetMapping("/{query}")
@ResponseBody
- public ResponseEntity getPlayer(@PathVariable @NonNull String query) throws BadRequestException, ResourceNotFoundException {
+ public ResponseEntity getPlayer(@PathVariable @NonNull String query) throws BadRequestException, ResourceNotFoundException {
return ResponseEntity.ofNullable(mojangService.getPlayer(query));
}
}
\ No newline at end of file
diff --git a/src/main/java/me/braydon/mc/exception/ExceptionControllerAdvice.java b/src/main/java/me/braydon/mc/exception/ExceptionControllerAdvice.java
index 38ebdac..21ddeca 100644
--- a/src/main/java/me/braydon/mc/exception/ExceptionControllerAdvice.java
+++ b/src/main/java/me/braydon/mc/exception/ExceptionControllerAdvice.java
@@ -1,7 +1,7 @@
package me.braydon.mc.exception;
import lombok.NonNull;
-import me.braydon.mc.model.ErrorResponse;
+import me.braydon.mc.model.response.ErrorResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
diff --git a/src/main/java/me/braydon/mc/model/Player.java b/src/main/java/me/braydon/mc/model/Player.java
index fe18568..5bc7425 100644
--- a/src/main/java/me/braydon/mc/model/Player.java
+++ b/src/main/java/me/braydon/mc/model/Player.java
@@ -1,6 +1,8 @@
package me.braydon.mc.model;
import lombok.*;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.redis.core.RedisHash;
import java.util.UUID;
@@ -9,12 +11,17 @@ import java.util.UUID;
*
* @author Braydon
*/
-@NoArgsConstructor @AllArgsConstructor @Setter @Getter @EqualsAndHashCode(onlyExplicitlyIncluded = true) @ToString
-public final class Player {
+@NoArgsConstructor
+@AllArgsConstructor
+@Setter @Getter
+@EqualsAndHashCode(onlyExplicitlyIncluded = true)
+@ToString
+@RedisHash(value = "player", timeToLive = 60L * 60L) // 1 hour (in seconds)
+public class Player {
/**
* The unique id of this player.
*/
- @EqualsAndHashCode.Include @NonNull private UUID uniqueId;
+ @Id @EqualsAndHashCode.Include @NonNull private UUID uniqueId;
/**
* The username of this player.
@@ -22,7 +29,7 @@ public final class Player {
@NonNull private String username;
/**
- * The profile actions this player has.
+ * The profile actions this player has, null if none.
*/
- @NonNull private ProfileAction[] profileActions;
+ private ProfileAction[] profileActions;
}
\ No newline at end of file
diff --git a/src/main/java/me/braydon/mc/model/cache/CachedPlayer.java b/src/main/java/me/braydon/mc/model/cache/CachedPlayer.java
new file mode 100644
index 0000000..20538e2
--- /dev/null
+++ b/src/main/java/me/braydon/mc/model/cache/CachedPlayer.java
@@ -0,0 +1,29 @@
+package me.braydon.mc.model.cache;
+
+import lombok.Getter;
+import lombok.NonNull;
+import lombok.Setter;
+import me.braydon.mc.model.Player;
+import me.braydon.mc.model.ProfileAction;
+
+import java.io.Serializable;
+import java.util.UUID;
+
+/**
+ * A cacheable {@link Player}.
+ *
+ * @author Braydon
+ */
+@Setter @Getter
+public final class CachedPlayer extends Player implements Serializable {
+ /**
+ * The unix timestamp of when this
+ * player was cached, -1 if not cached.
+ */
+ private long cached;
+
+ public CachedPlayer(@NonNull UUID uniqueId, @NonNull String username, ProfileAction[] profileActions, long cached) {
+ super(uniqueId, username, profileActions);
+ this.cached = cached;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/me/braydon/mc/model/ErrorResponse.java b/src/main/java/me/braydon/mc/model/response/ErrorResponse.java
similarity index 95%
rename from src/main/java/me/braydon/mc/model/ErrorResponse.java
rename to src/main/java/me/braydon/mc/model/response/ErrorResponse.java
index 57a2677..c986c1b 100644
--- a/src/main/java/me/braydon/mc/model/ErrorResponse.java
+++ b/src/main/java/me/braydon/mc/model/response/ErrorResponse.java
@@ -1,4 +1,4 @@
-package me.braydon.mc.model;
+package me.braydon.mc.model.response;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.*;
diff --git a/src/main/java/me/braydon/mc/repository/PlayerCacheRepository.java b/src/main/java/me/braydon/mc/repository/PlayerCacheRepository.java
new file mode 100644
index 0000000..2062501
--- /dev/null
+++ b/src/main/java/me/braydon/mc/repository/PlayerCacheRepository.java
@@ -0,0 +1,14 @@
+package me.braydon.mc.repository;
+
+import me.braydon.mc.model.Player;
+import me.braydon.mc.model.cache.CachedPlayer;
+import org.springframework.data.repository.CrudRepository;
+
+import java.util.UUID;
+
+/**
+ * A cache repository for {@link Player}'s.
+ *
+ * @author Braydon
+ */
+public interface PlayerCacheRepository extends CrudRepository { }
\ No newline at end of file
diff --git a/src/main/java/me/braydon/mc/service/MojangService.java b/src/main/java/me/braydon/mc/service/MojangService.java
index 9d4bc7f..70d7b83 100644
--- a/src/main/java/me/braydon/mc/service/MojangService.java
+++ b/src/main/java/me/braydon/mc/service/MojangService.java
@@ -8,11 +8,16 @@ import me.braydon.mc.common.web.JsonWebRequest;
import me.braydon.mc.exception.impl.BadRequestException;
import me.braydon.mc.exception.impl.ResourceNotFoundException;
import me.braydon.mc.model.Player;
+import me.braydon.mc.model.ProfileAction;
+import me.braydon.mc.model.cache.CachedPlayer;
import me.braydon.mc.model.token.MojangProfileToken;
import me.braydon.mc.model.token.MojangUsernameToUUIDToken;
+import me.braydon.mc.repository.PlayerCacheRepository;
+import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Service;
+import java.util.Optional;
import java.util.UUID;
/**
@@ -28,6 +33,16 @@ public final class MojangService {
private static final String UUID_TO_PROFILE = SESSION_SERVER_ENDPOINT + "/session/minecraft/profile/%s";
private static final String USERNAME_TO_UUID = API_ENDPOINT + "/users/profiles/minecraft/%s";
+ /**
+ * The cache repository for {@link Player}'s.
+ */
+ @NonNull private final PlayerCacheRepository playerCacheRepository;
+
+ @Autowired
+ public MojangService(@NonNull PlayerCacheRepository playerCacheRepository) {
+ this.playerCacheRepository = playerCacheRepository;
+ }
+
/**
* Get a player by their username or UUID.
*
@@ -37,7 +52,7 @@ public final class MojangService {
* @throws ResourceNotFoundException if the player is not found
*/
@NonNull
- public Player getPlayer(@NonNull String query) throws BadRequestException, ResourceNotFoundException {
+ public CachedPlayer getPlayer(@NonNull String query) throws BadRequestException, ResourceNotFoundException {
log.info("Requesting player with query: {}", query);
UUID uuid; // The player UUID to lookup
@@ -54,6 +69,13 @@ public final class MojangService {
log.info("Found UUID for username {}: {}", query, uuid);
}
+ // Check the cache for the player
+ Optional cached = playerCacheRepository.findById(uuid);
+ if (cached.isPresent()) { // Respond with the cache if present
+ log.info("Found player in cache: {}", uuid);
+ return cached.get();
+ }
+
// Send a request to Mojang requesting
// the player profile by their UUID
try {
@@ -61,9 +83,19 @@ public final class MojangService {
MojangProfileToken token = JsonWebRequest.makeRequest(
UUID_TO_PROFILE.formatted(uuid), HttpMethod.GET
).execute(MojangProfileToken.class);
+ ProfileAction[] profileActions = token.getProfileActions();
- // Return our player model representing the requested player
- return new Player(uuid, token.getName(), token.getProfileActions());
+ // Build our player model, cache it, and then return it
+ CachedPlayer player = new CachedPlayer(
+ uuid, token.getName(),
+ profileActions.length == 0 ? null : profileActions,
+ System.currentTimeMillis()
+ );
+ playerCacheRepository.save(player);
+ log.info("Cached player: {}", uuid);
+
+ player.setCached(-1L); // Set to -1 to indicate it's not cached in the response
+ return player;
} catch (JsonWebException ex) {
// No profile found, return null
if (ex.getStatusCode() == 400) {