diff --git a/api/src/main/java/com/alttd/boosterapi/config/BoosterFileStorage.java b/api/src/main/java/com/alttd/boosterapi/config/BoosterFileStorage.java index 72455a9..67f0de6 100644 --- a/api/src/main/java/com/alttd/boosterapi/config/BoosterFileStorage.java +++ b/api/src/main/java/com/alttd/boosterapi/config/BoosterFileStorage.java @@ -4,11 +4,10 @@ import com.alttd.boosterapi.data.Booster; import com.alttd.boosterapi.data.BoosterType; import com.alttd.boosterapi.util.Logger; import com.fasterxml.jackson.core.*; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; import java.io.File; import java.io.IOException; +import java.nio.file.*; import java.time.Duration; import java.time.Instant; import java.util.LinkedList; @@ -20,6 +19,9 @@ public class BoosterFileStorage { private File CONFIG_FILE; private final Logger logger; + private WatchService watchService; + private Thread watchThread; + public BoosterFileStorage(Logger logger) { this.logger = logger; logger.info("Preparing booster file storage..."); @@ -29,103 +31,198 @@ public class BoosterFileStorage { private void init() { File CONFIG_PATH = new File(File.separator + "mnt" + File.separator + "configs" + File.separator + "Boosters"); if (!CONFIG_PATH.exists()) { - if (!CONFIG_PATH.mkdir()) + if (!CONFIG_PATH.mkdir()) { logger.severe("Unable to create json storage directory"); + } } CONFIG_FILE = new File(CONFIG_PATH, "storage.json"); if (!CONFIG_FILE.exists()) { try { - if (!CONFIG_FILE.createNewFile()) - logger.severe("Unable to create json storeage file"); + if (!CONFIG_FILE.createNewFile()) { + logger.severe("Unable to create json storage file"); + } } catch (IOException e) { - e.printStackTrace(); + throw new RuntimeException(e); } } - - ObjectMapper mapper = new ObjectMapper(); - mapper.enable(SerializationFeature.INDENT_OUTPUT); } - public synchronized List reload() { + /** + * Attempts to reload the whole storage file. + * + * Optional.empty() means the file was invalid/incomplete and callers should + * keep their current in-memory state rather than replacing it. + */ + public synchronized Optional> reload() { logger.debug("Reloading boosters..."); return loadBoosters(); } - private List loadBoosters() { - List boosters = new LinkedList<>(); + /** + * Watches storage.json and asks the owner of the cache to reload whenever + * the file changes or is replaced. + */ + public synchronized void startWatching(Runnable onReload) { + if (watchThread != null && watchThread.isAlive()) + return; try { - JsonParser parser = new JsonFactory().createParser(CONFIG_FILE); - if (parser == null) { - logger.warning("Unable to load in boosters from storage file."); - return boosters; + Path directory = CONFIG_FILE.toPath().getParent(); + watchService = FileSystems.getDefault().newWatchService(); + directory.register( + watchService, + StandardWatchEventKinds.ENTRY_MODIFY, + StandardWatchEventKinds.ENTRY_CREATE + ); + + watchThread = new Thread(() -> watchLoop(onReload), "booster-storage-watcher"); + watchThread.setDaemon(true); + watchThread.start(); + } catch (IOException e) { + throw new RuntimeException("Unable to watch booster storage file", e); + } + } + + private void watchLoop(Runnable onReload) { + Path fileName = CONFIG_FILE.toPath().getFileName(); + + while (!Thread.currentThread().isInterrupted()) { + try { + WatchKey key = watchService.take(); + boolean changed = false; + + for (WatchEvent event : key.pollEvents()) { + if (event.kind() == StandardWatchEventKinds.OVERFLOW) + continue; + + Path changedFile = (Path) event.context(); + if (fileName.equals(changedFile)) + changed = true; + } + + if (!key.reset()) + break; + + if (changed) { + try { + onReload.run(); + } catch (RuntimeException e) { + logger.severe("Unable to reload boosters after storage file changed: " + e.getMessage()); + } + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (ClosedWatchServiceException e) { + break; } - parser.nextToken(); - while (parser.currentToken() != null && parser.currentToken().isStructStart()) { + } + } + + private Optional> loadBoosters() { + List boosters = new LinkedList<>(); + + try (JsonParser parser = new JsonFactory().createParser(CONFIG_FILE)) { + JsonToken token = parser.nextToken(); + + // An empty file represents an empty booster list. + if (token == null) + return Optional.of(boosters); + + while (token != null) { + if (token != JsonToken.START_OBJECT) + return invalid("Expected booster object start but found " + token); + Optional optionalBooster = loadBooster(parser); - if (optionalBooster.isEmpty()) - continue; + if (optionalBooster.isEmpty()) { + return Optional.empty(); + } + Booster booster = optionalBooster.get(); logger.debug("Loading booster:" + booster.getBoosterType()); boosters.add(booster); - if (parser.nextToken() != null && !parser.currentToken().isStructEnd()) { - logger.warning("Last loaded booster had more data than expected, skipping it..."); - while (!parser.nextToken().isStructEnd()) - ; + + token = parser.nextToken(); + if (token != JsonToken.END_OBJECT) { + return invalid("Expected booster object end but found " + token); } - parser.nextToken(); + + token = parser.nextToken(); } - parser.close(); - } catch (IOException e) { - throw new RuntimeException(e); + + return Optional.of(boosters); + } catch (IOException | IllegalArgumentException e) { + logger.warning("Unable to load boosters from storage file: " + e.getMessage()); + return Optional.empty(); } - return boosters; } public Optional loadBooster(JsonParser parser) throws IOException { - JsonToken jsonToken = parser.getCurrentToken(); - if (!jsonToken.isStructStart()) - return error("Didn't find struct start"); - - jsonToken = parser.nextToken(); - if (jsonToken != JsonToken.FIELD_NAME || !"boosterUUID".equals(parser.getCurrentName())) + JsonToken jsonToken = parser.nextToken(); + if (jsonToken != JsonToken.FIELD_NAME || !"boosterUUID".equals(parser.getCurrentName())) { return error("Didn't find boosterUUID at expected location"); - parser.nextValue(); + } + if (parser.nextToken() != JsonToken.VALUE_STRING) { + return error("boosterUUID was not a string"); + } UUID boosterUUID = UUID.fromString(parser.getValueAsString()); jsonToken = parser.nextToken(); - if (jsonToken != JsonToken.FIELD_NAME || !"activatorName".equals(parser.getCurrentName())) + if (jsonToken != JsonToken.FIELD_NAME || !"activatorName".equals(parser.getCurrentName())) { return error("Didn't find activatorName at expected location"); - parser.nextValue(); + } + if (parser.nextToken() != JsonToken.VALUE_STRING) { + return error("activatorName was not a string"); + } String activatorName = parser.getValueAsString(); jsonToken = parser.nextToken(); - if (jsonToken != JsonToken.FIELD_NAME || !"boosterType".equals(parser.getCurrentName())) + if (jsonToken != JsonToken.FIELD_NAME || !"boosterType".equals(parser.getCurrentName())) { return error("Didn't find boosterType at expected location"); - parser.nextValue(); + } + if (parser.nextToken() != JsonToken.VALUE_STRING) { + return error("boosterType was not a string"); + } BoosterType boosterType = BoosterType.getByName(parser.getValueAsString()); + if (boosterType == null) { + return error("Unknown boosterType"); + } jsonToken = parser.nextToken(); - if (jsonToken != JsonToken.FIELD_NAME || !"startingTime".equals(parser.getCurrentName())) + if (jsonToken != JsonToken.FIELD_NAME || !"startingTime".equals(parser.getCurrentName())) { return error("Didn't find startingTime at expected location"); - parser.nextValue(); - Instant startingTime = Instant.ofEpochMilli(parser.getValueAsLong()); + } + if (parser.nextToken() != JsonToken.VALUE_NUMBER_INT) { + return error("startingTime was not an integer"); + } + Instant startingTime = Instant.ofEpochMilli(parser.getLongValue()); jsonToken = parser.nextToken(); - if (jsonToken != JsonToken.FIELD_NAME || !"duration".equals(parser.getCurrentName())) + if (jsonToken != JsonToken.FIELD_NAME || !"duration".equals(parser.getCurrentName())) { return error("Didn't find duration at expected location"); - parser.nextValue(); - Duration duration = Duration.ofMillis(parser.getValueAsLong()); + } + if (parser.nextToken() != JsonToken.VALUE_NUMBER_INT) { + return error("duration was not an integer"); + } + Duration duration = Duration.ofMillis(parser.getLongValue()); jsonToken = parser.nextToken(); - if (jsonToken != JsonToken.FIELD_NAME || !"multiplier".equals(parser.getCurrentName())) + if (jsonToken != JsonToken.FIELD_NAME || !"multiplier".equals(parser.getCurrentName())) { return error("Didn't find multiplier at expected location"); - parser.nextValue(); - double multiplier = parser.getValueAsDouble(); + } + JsonToken multiplierToken = parser.nextToken(); + if (multiplierToken != JsonToken.VALUE_NUMBER_INT && multiplierToken != JsonToken.VALUE_NUMBER_FLOAT) { + return error("multiplier was not a number"); + } + double multiplier = parser.getDoubleValue(); return Optional.of(new Booster(boosterUUID, activatorName, boosterType, startingTime, duration, multiplier)); } + private Optional invalid(String error) { + logger.severe(error); + return Optional.empty(); + } + private Optional error(String error) { logger.severe(error); return Optional.empty(); @@ -143,7 +240,6 @@ public class BoosterFileStorage { } } - private void saveBooster(Booster booster, JsonGenerator generator) throws IOException { generator.writeStartObject(); diff --git a/api/src/main/java/com/alttd/boosterapi/data/BoosterCache.java b/api/src/main/java/com/alttd/boosterapi/data/BoosterCache.java index b151056..0ad8bff 100644 --- a/api/src/main/java/com/alttd/boosterapi/data/BoosterCache.java +++ b/api/src/main/java/com/alttd/boosterapi/data/BoosterCache.java @@ -17,16 +17,23 @@ public class BoosterCache { public BoosterCache(BoosterFileStorage boosterFileStorage) { this.boosterFileStorage = boosterFileStorage; reloadBoosters(); + boosterFileStorage.startWatching(this::reloadBoosters); } public synchronized void reloadBoosters() { - boosters.clear(); - List allBoosters = boosterFileStorage.reload(); - for (Booster booster : allBoosters) { - LinkedList list = boosters.getOrDefault(booster.getBoosterType(), new LinkedList<>()); + Optional> reloadedBoosters = boosterFileStorage.reload(); + if (reloadedBoosters.isEmpty()) + return; + + HashMap> newBoosters = new HashMap<>(); + for (Booster booster : reloadedBoosters.get()) { + LinkedList list = newBoosters.getOrDefault(booster.getBoosterType(), new LinkedList<>()); list.add(booster); - boosters.put(booster.getBoosterType(), list); + newBoosters.put(booster.getBoosterType(), list); } + + boosters.clear(); + boosters.putAll(newBoosters); updateOrder(); }