Introduce MessageStore and move logic to FileBasedMessageStore

This change is a preparation for introducing different message storage
types.

Change-Id: I7d5809a508f85e217320d4644b8ffde85bee58ee
diff --git a/src/main/java/com/googlesource/gerrit/plugins/messageoftheday/ConfiguredMessage.java b/src/main/java/com/googlesource/gerrit/plugins/messageoftheday/ConfiguredMessage.java
new file mode 100644
index 0000000..ae3901f
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/messageoftheday/ConfiguredMessage.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.messageoftheday;
+
+import com.google.auto.value.AutoValue;
+import org.eclipse.jgit.lib.Config;
+
+@AutoValue
+public abstract class ConfiguredMessage {
+  public static ConfiguredMessage create(Config cfg, String message) {
+    return new AutoValue_ConfiguredMessage(cfg, message);
+  }
+
+  public abstract Config config();
+
+  public abstract String message();
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/messageoftheday/FileBasedMessageStore.java b/src/main/java/com/googlesource/gerrit/plugins/messageoftheday/FileBasedMessageStore.java
new file mode 100644
index 0000000..4324086
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/messageoftheday/FileBasedMessageStore.java
@@ -0,0 +1,101 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.messageoftheday;
+
+import static com.google.common.io.Files.asCharSink;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.base.Strings;
+import com.google.common.flogger.FluentLogger;
+import com.google.common.io.CharSink;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+
+@Singleton
+public class FileBasedMessageStore implements MessageStore {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+  private static final String SECTION_MESSAGE = "message";
+  private static final String KEY_ID = "id";
+
+  private final File cfgFile;
+  private final Path dataDir;
+
+  @Inject
+  FileBasedMessageStore(@ConfigFile File cfgFile, @DataDir Path dataDir) {
+    this.cfgFile = cfgFile;
+    this.dataDir = dataDir;
+  }
+
+  @Override
+  public ConfiguredMessage getConfiguredMessage() throws MessageStoreException {
+    FileBasedConfig cfg = loadConfig();
+
+    String htmlFileId = cfg.getString(SECTION_MESSAGE, null, KEY_ID);
+    if (Strings.isNullOrEmpty(htmlFileId)) {
+      logger.atWarning().log("id not defined, no message will be shown");
+      return ConfiguredMessage.create(cfg, null);
+    }
+
+    String message = loadMessage(htmlFileId);
+    return ConfiguredMessage.create(cfg, message);
+  }
+
+  private FileBasedConfig loadConfig() throws MessageStoreException {
+    FileBasedConfig cfg = new FileBasedConfig(cfgFile, FS.DETECTED);
+    try {
+      cfg.load();
+    } catch (ConfigInvalidException | IOException e) {
+      throw new MessageStoreException("plugin cfg is invalid or could not be loaded", e);
+    }
+    return cfg;
+  }
+
+  private String loadMessage(String id) {
+    try {
+      return new String(Files.readAllBytes(dataDir.resolve(id + ".html")), UTF_8);
+    } catch (IOException e1) {
+      logger.atWarning().log("No HTML-file was found for message %s, no message will be shown", id);
+      return null;
+    }
+  }
+
+  @Override
+  public void saveConfiguredMessage(ConfiguredMessage message) throws MessageStoreException {
+    FileBasedConfig configFile = new FileBasedConfig(message.config(), cfgFile, FS.DETECTED);
+
+    String id = configFile.getString(SECTION_MESSAGE, null, KEY_ID);
+    try {
+      Path path = dataDir.resolve(id + ".html");
+      CharSink sink = asCharSink(path.toFile(), StandardCharsets.UTF_8);
+      sink.write(message.message());
+    } catch (IOException e) {
+      throw new MessageStoreException("Failed to save message", e);
+    }
+
+    try {
+      configFile.save();
+    } catch (IOException e) {
+      throw new MessageStoreException("Failed to save config", e);
+    }
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/messageoftheday/GetMessage.java b/src/main/java/com/googlesource/gerrit/plugins/messageoftheday/GetMessage.java
index 2012f31..3369392 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/messageoftheday/GetMessage.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/messageoftheday/GetMessage.java
@@ -14,30 +14,20 @@
 
 package com.googlesource.gerrit.plugins.messageoftheday;
 
-import static java.nio.charset.StandardCharsets.UTF_8;
-
 import com.google.common.base.Strings;
-import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.inject.Inject;
-import java.io.File;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.util.Date;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.storage.file.FileBasedConfig;
-import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.lib.Config;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 public class GetMessage implements RestReadView<ConfigResource> {
   private static final String SECTION_MESSAGE = "message";
-  private static final String KEY_ID = "id";
   private static final String KEY_STARTS_AT = "startsAt";
   private static final String KEY_EXPIRES_AT = "expiresAt";
 
@@ -45,31 +35,26 @@
 
   private static final Logger log = LoggerFactory.getLogger(GetMessage.class);
 
-  private final File cfgFile;
-  private final Path dataDirPath;
-
-  private volatile FileBasedConfig cfg;
+  private final MessageStore messageStore;
 
   @Inject
-  public GetMessage(
-      @PluginName String pluginName, @ConfigFile File cfgFile, @DataDir Path dataDirPath) {
-    this.dataDirPath = dataDirPath;
-    this.cfgFile = cfgFile;
+  public GetMessage(MessageStore messageStore) {
+    this.messageStore = messageStore;
   }
 
   @Override
   public Response<MessageOfTheDayInfo> apply(ConfigResource rsrc) {
     MessageOfTheDayInfo motd = new MessageOfTheDayInfo();
-    cfg = new FileBasedConfig(cfgFile, FS.DETECTED);
+    ConfiguredMessage configuredMessage;
     try {
-      cfg.load();
-    } catch (ConfigInvalidException | IOException e) {
-      return null;
+      configuredMessage = messageStore.getConfiguredMessage();
+    } catch (MessageStoreException e) {
+      log.warn(e.getMessage());
+      return Response.none();
     }
-
-    String htmlFileId = cfg.getString(SECTION_MESSAGE, null, KEY_ID);
-    if (Strings.isNullOrEmpty(htmlFileId)) {
-      log.warn("id not defined, no message will be shown");
+    Config cfg = configuredMessage.config();
+    String message = configuredMessage.message();
+    if (cfg == null || message == null) {
       return Response.none();
     }
 
@@ -92,15 +77,7 @@
       return Response.none();
     }
 
-    try {
-      motd.html = new String(Files.readAllBytes(dataDirPath.resolve(htmlFileId + ".html")), UTF_8);
-    } catch (IOException e1) {
-      log.warn(
-          String.format(
-              "No HTML-file was found for message %s, no message will be shown", htmlFileId));
-      return Response.none();
-    }
-
+    motd.html = message;
     motd.id = Integer.toString(motd.html.hashCode());
     return Response.ok(motd);
   }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/messageoftheday/MessageStore.java b/src/main/java/com/googlesource/gerrit/plugins/messageoftheday/MessageStore.java
new file mode 100644
index 0000000..1bd00c1
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/messageoftheday/MessageStore.java
@@ -0,0 +1,21 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.messageoftheday;
+
+public interface MessageStore {
+  ConfiguredMessage getConfiguredMessage() throws MessageStoreException;
+
+  void saveConfiguredMessage(ConfiguredMessage message) throws MessageStoreException;
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/messageoftheday/MessageStoreException.java b/src/main/java/com/googlesource/gerrit/plugins/messageoftheday/MessageStoreException.java
new file mode 100644
index 0000000..2237071
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/messageoftheday/MessageStoreException.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2025 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.messageoftheday;
+
+public class MessageStoreException extends Exception {
+  private static final long serialVersionUID = 1L;
+
+  public MessageStoreException(String message) {
+    super(message);
+  }
+
+  public MessageStoreException(String message, Exception cause) {
+    super(message, cause);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/messageoftheday/Module.java b/src/main/java/com/googlesource/gerrit/plugins/messageoftheday/Module.java
index 8ecf5a6..085608b 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/messageoftheday/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/messageoftheday/Module.java
@@ -44,6 +44,7 @@
 
   @Override
   protected void configure() {
+    bind(MessageStore.class).to(FileBasedMessageStore.class);
     bind(CapabilityDefinition.class)
         .annotatedWith(Exports.named(UpdateBannerCapability.NAME))
         .to(UpdateBannerCapability.class);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/messageoftheday/SetMessage.java b/src/main/java/com/googlesource/gerrit/plugins/messageoftheday/SetMessage.java
index 4096a5d..39efa69 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/messageoftheday/SetMessage.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/messageoftheday/SetMessage.java
@@ -16,8 +16,6 @@
 
 import com.google.common.base.Strings;
 import com.google.common.flogger.FluentLogger;
-import com.google.common.io.CharSink;
-import com.google.common.io.Files;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -30,18 +28,13 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
-import java.io.File;
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Path;
 import java.time.ZoneId;
 import java.time.ZonedDateTime;
 import java.time.format.DateTimeFormatter;
 import java.util.Locale;
 import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.storage.file.FileBasedConfig;
-import org.eclipse.jgit.util.FS;
 
 public class SetMessage implements RestModifyView<ConfigResource, MessageInput> {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -53,24 +46,19 @@
   private static final DateTimeFormatter INPUT_DATE_FORMAT =
       DateTimeFormatter.ofPattern(INPUT_DATE_FORMAT_PATTERN, Locale.ENGLISH);
 
-  private final File cfgFile;
-  private final Path dataDirPath;
+  private final MessageStore messageStore;
   private final ZoneId serverZoneId;
   private final PermissionBackend permissionBackend;
   private final UpdateBannerPermission permission;
 
-  private volatile FileBasedConfig cfg;
-
   @Inject
   public SetMessage(
-      @ConfigFile File cfgFile,
-      @DataDir Path dataDirPath,
+      MessageStore messageStore,
       @GerritPersonIdent Provider<PersonIdent> serverIdent,
       PermissionBackend permissionBackend,
       UpdateBannerPermission permission) {
-    this.dataDirPath = dataDirPath;
+    this.messageStore = messageStore;
     this.serverZoneId = serverIdent.get().getZoneId();
-    this.cfgFile = cfgFile;
     this.permission = permission;
     this.permissionBackend = permissionBackend;
   }
@@ -89,22 +77,21 @@
       throw new BadRequestException("message is required");
     }
 
-    cfg = new FileBasedConfig(cfgFile, FS.DETECTED);
+    ConfiguredMessage configuredMessage;
     try {
-      cfg.load();
-    } catch (ConfigInvalidException | IOException e) {
-      throw new UnprocessableEntityException("plugin cfg is invalid or could not be loaded", e);
+      configuredMessage = messageStore.getConfiguredMessage();
+    } catch (MessageStoreException e) {
+      throw new UnprocessableEntityException(e.getMessage(), e);
     }
 
+    Config cfg = configuredMessage.config();
+
     String id = cfg.getString(SECTION_MESSAGE, null, KEY_ID);
     if (Strings.isNullOrEmpty(id)) {
       logger.atInfo().log("'id' is not configured in the plugin cfg. Choosing a default id.");
-      id = "default";
+      cfg.setString(SECTION_MESSAGE, null, KEY_ID, "default");
     }
 
-    Path path = dataDirPath.resolve(id + ".html");
-    CharSink sink = Files.asCharSink(path.toFile(), StandardCharsets.UTF_8);
-
     if (input.expiresAt != null) {
       ZonedDateTime time;
       try {
@@ -122,7 +109,6 @@
           null,
           KEY_EXPIRES_AT,
           time.format(DateTimeFormatter.ofPattern("yyyyMMdd:HHmm")));
-      cfg.setString(SECTION_MESSAGE, null, KEY_ID, id);
     } else {
       String expiredAt = cfg.getString(SECTION_MESSAGE, null, KEY_EXPIRES_AT);
       if (expiredAt == null) {
@@ -133,17 +119,9 @@
     }
 
     try {
-      sink.write(input.message);
-    } catch (IOException e) {
-      throw new UnprocessableEntityException("Failed to save message", e);
-    }
-
-    if (input.expiresAt != null) {
-      try {
-        cfg.save();
-      } catch (IOException e) {
-        throw new UnprocessableEntityException("Failed to save plugin config", e);
-      }
+      messageStore.saveConfiguredMessage(ConfiguredMessage.create(cfg, input.message));
+    } catch (MessageStoreException e) {
+      throw new UnprocessableEntityException(e.getMessage(), e);
     }
 
     return Response.ok();