diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 0000000..b7848ee
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,35 @@
+// For format details, see https://aka.ms/devcontainer.json. For config options, see the
+// README at: https://github.com/devcontainers/templates/tree/main/src/java
+{
+ "name": "Java",
+ // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
+ "image": "mcr.microsoft.com/devcontainers/java:1-21-bullseye",
+
+ "features": {
+ "ghcr.io/devcontainers/features/java:1": {
+ "version": "none",
+ "installMaven": "true",
+ "installGradle": "false"
+ }
+ },
+ "customizations": {
+ "vscode": {
+ "extensions": [
+ "redhat.java",
+ "redhat.vscode-xml"
+ ]
+ }
+ }
+
+ // Use 'forwardPorts' to make a list of ports inside the container available locally.
+ // "forwardPorts": [],
+
+ // Use 'postCreateCommand' to run commands after the container is created.
+ // "postCreateCommand": "java -version",
+
+ // Configure tool-specific properties.
+ // "customizations": {},
+
+ // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
+ // "remoteUser": "root"
+}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..bf9b9f4
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,130 @@
+# File created using '.gitignore Generator' for Visual Studio Code: https://bit.ly/vscode-gig
+# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,linux,gradle,java,kotlin,maven
+# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,linux,gradle,java,kotlin,maven
+
+### Java ###
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+replay_pid*
+
+### Kotlin ###
+# Compiled class file
+
+# Log file
+
+# BlueJ files
+
+# Mobile Tools for Java (J2ME)
+
+# Package Files #
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+
+### Linux ###
+*~
+
+# temporary files which can be created if a process still has a handle open of a deleted file
+.fuse_hidden*
+
+# KDE directory preferences
+.directory
+
+# Linux trash folder which might appear on any partition or disk
+.Trash-*
+
+# .nfs files are created when an open file is removed but is still being accessed
+.nfs*
+
+### Maven ###
+target/
+pom.xml.tag
+pom.xml.releaseBackup
+pom.xml.versionsBackup
+pom.xml.next
+release.properties
+dependency-reduced-pom.xml
+buildNumber.properties
+.mvn/timing.properties
+# https://github.com/takari/maven-wrapper#usage-without-binary-jar
+.mvn/wrapper/maven-wrapper.jar
+
+# Eclipse m2e generated files
+# Eclipse Core
+.project
+# JDT-specific (Eclipse Java Development Tools)
+.classpath
+
+### VisualStudioCode ###
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+!.vscode/*.code-snippets
+
+# Local History for Visual Studio Code
+.history/
+
+# Built Visual Studio Code Extensions
+*.vsix
+
+### VisualStudioCode Patch ###
+# Ignore all local history of files
+.history
+.ionide
+
+### Gradle ###
+.gradle
+**/build/
+!src/**/build/
+
+# Ignore Gradle GUI config
+gradle-app.setting
+
+# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
+!gradle-wrapper.jar
+
+# Avoid ignore Gradle wrappper properties
+!gradle-wrapper.properties
+
+# Cache of project
+.gradletasknamecache
+
+# Eclipse Gradle plugin generated files
+# Eclipse Core
+# JDT-specific (Eclipse Java Development Tools)
+
+### Gradle Patch ###
+# Java heap dump
+*.hprof
+
+# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,linux,gradle,java,kotlin,maven
+
+# Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option)
+
+buildtools/*
+!buildtools/install.sh
+devserver/*
+!devserver/start.sh
+!devserver/loadplugin.sh
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..0be1c0c
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,4 @@
+{
+ "java.configuration.updateBuildConfiguration": "automatic",
+ "java.compile.nullAnalysis.mode": "automatic"
+}
\ No newline at end of file
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
new file mode 100644
index 0000000..eab3cbe
--- /dev/null
+++ b/.vscode/tasks.json
@@ -0,0 +1,15 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "type": "java (buildArtifact)",
+ "targetPath": "${workspaceFolder}/devserver/plugins/${workspaceFolderBasename}.jar",
+ "elements": [
+ "${compileOutput}",
+ "${dependencies}"
+ ],
+ "problemMatcher": [],
+ "label": "Build SpigotResourceSync to plugins"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/buildtools/install.sh b/buildtools/install.sh
new file mode 100644
index 0000000..e2f7dad
--- /dev/null
+++ b/buildtools/install.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+
+curl -o BuildTools.jar https://hub.spigotmc.org/jenkins/job/BuildTools/lastSuccessfulBuild/artifact/target/BuildTools.jar
+java -jar BuildTools.jar --rev 1.20.4 --final-name spigot.jar
+cp spigot.jar ../devserver/.
+echo "Moved spigot.jar to ../devserver/."
\ No newline at end of file
diff --git a/devserver/loadplugin.sh b/devserver/loadplugin.sh
new file mode 100644
index 0000000..3b28d72
--- /dev/null
+++ b/devserver/loadplugin.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+cp ../target/*.jar ./plugins/.
\ No newline at end of file
diff --git a/devserver/start.sh b/devserver/start.sh
new file mode 100755
index 0000000..4bddb68
--- /dev/null
+++ b/devserver/start.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+java -Xmx2G -XX:+UseG1GC -jar spigot.jar nogui
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..140d40d
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,58 @@
+
+
+ 4.0.0
+
+ systems.reslate.entertainment.spigotresourcesync
+ spigotresourcesync
+ 1.0-SNAPSHOT
+
+
+ 21
+ 21
+
+
+
+
+ spigot-repo
+ https://hub.spigotmc.org/nexus/content/repositories/snapshots/
+
+
+
+
+
+ org.spigotmc
+ spigot-api
+ 1.20.4-R0.1-SNAPSHOT
+ provided
+
+
+ commons-io
+ commons-io
+ 2.17.0
+
+
+ com.fasterxml.jackson.dataformat
+ jackson-dataformat-yaml
+ 2.18.0
+
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-jsr310
+ 2.18.0
+
+
+ com.fasterxml.jackson.core
+ jackson-core
+ 2.18.0
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ 5.11.0-M2
+ test
+
+
+
+
\ No newline at end of file
diff --git a/src/main/java/solutions/reslate/entertainment/spigotresourcesync/ConfigManager.java b/src/main/java/solutions/reslate/entertainment/spigotresourcesync/ConfigManager.java
new file mode 100644
index 0000000..aee0bf5
--- /dev/null
+++ b/src/main/java/solutions/reslate/entertainment/spigotresourcesync/ConfigManager.java
@@ -0,0 +1,121 @@
+package solutions.reslate.entertainment.spigotresourcesync;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.Reader;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.logging.Logger;
+
+import org.bukkit.configuration.InvalidConfigurationException;
+import org.bukkit.configuration.file.FileConfiguration;
+
+import solutions.reslate.entertainment.spigotresourcesync.data.Configuration;
+import solutions.reslate.entertainment.spigotresourcesync.events.ObjectLoadListener;
+import solutions.reslate.entertainment.spigotresourcesync.serialisation.Serialiser;
+
+public class ConfigManager extends FileConfiguration {
+ private Serialiser serialiser;
+ private Configuration config;
+ private File file;
+ private Logger logger;
+ private Collection> configLoadListener;
+
+
+ public ConfigManager(File file, Serialiser serialiser, Logger logger) {
+ super();
+ this.serialiser = serialiser;
+ this.config = new Configuration();
+ this.file = file;
+ this.logger = logger;
+ this.configLoadListener = new HashSet<>();
+ }
+
+ public void flush() {
+ try {
+ save(this.file);
+ } catch (IOException e) {
+ logger.severe(String.format("Unable to save configuration to \"%s\". %s", this.file.getAbsolutePath(), e.getMessage()));
+ }
+ }
+
+ @Override
+ public void save(String file) throws IOException {
+ this.save(new File(file));
+ }
+
+ @Override
+ public void save(File file) throws IOException {
+ this.serialiser.flushTo(file, this.config);
+ logger.info(String.format("Saved configuration to \"%s\".", this.file.getAbsolutePath()));
+ }
+
+ public void load() {
+ try {
+ this.load(file);
+ } catch (IOException | InvalidConfigurationException e) {
+ logger.severe(String.format("Unable to load configuration from \"%s\".", file.getAbsolutePath(), e.getMessage()));
+ if (file.exists()) {
+ logger.severe(String.format("Found pre-existing file. No overwriting will occur. Delete \"%s\" and restart to generate new configuration.", this.file.getAbsolutePath()));
+ } else {
+ flush();
+ }
+ }
+ }
+
+
+ @Override
+ public void load(String file) throws FileNotFoundException, IOException, InvalidConfigurationException {
+ this.load(new File(file));
+ }
+
+ @Override
+ public void load(File file) throws FileNotFoundException, IOException, InvalidConfigurationException {
+ try (FileReader reader = new FileReader(file)) {
+ this.load(reader);
+ }
+ }
+
+ @Override
+ public void load(Reader reader) throws IOException, InvalidConfigurationException {
+ this.config = this.serialiser.load(reader, Configuration.class);
+ logger.info(String.format("Successfully loaded configuration from \"%s\".", this.file.getAbsolutePath()));
+ onLoadConfig();
+ }
+
+ public Configuration getConfiguration() {
+ return config;
+ }
+
+ private void onLoadConfig() {
+ for (ObjectLoadListener objectUpdateListener : configLoadListener) {
+ objectUpdateListener.objectLoaded(config);
+ }
+ }
+
+ @Override
+ public String saveToString() {
+ return serialiser.serialize(this.config);
+ }
+
+ @Override
+ public void loadFromString(String contents) throws InvalidConfigurationException {
+ this.config = serialiser.deserialize(contents, Configuration.class);
+ onLoadConfig();
+ }
+
+ public void addConfigLoadListener(ObjectLoadListener listener) {
+ this.configLoadListener.add(listener);
+ }
+
+ public void removeConfigLoadListener(ObjectLoadListener listener) {
+ this.configLoadListener.remove(listener);
+ }
+
+ public void resetConfiguration() {
+ this.config = new Configuration();
+ }
+
+}
diff --git a/src/main/java/solutions/reslate/entertainment/spigotresourcesync/SpigotResourceSync.java b/src/main/java/solutions/reslate/entertainment/spigotresourcesync/SpigotResourceSync.java
new file mode 100644
index 0000000..a2d04be
--- /dev/null
+++ b/src/main/java/solutions/reslate/entertainment/spigotresourcesync/SpigotResourceSync.java
@@ -0,0 +1,54 @@
+package solutions.reslate.entertainment.spigotresourcesync;
+
+import java.io.File;
+
+import org.bukkit.configuration.file.FileConfiguration;
+import org.bukkit.plugin.java.JavaPlugin;
+
+import solutions.reslate.entertainment.spigotresourcesync.serialisation.JacksonYamlSerialiser;
+import solutions.reslate.entertainment.spigotresourcesync.synchronisation.ApacheCommonsIOSynchroniser;
+
+public class SpigotResourceSync extends JavaPlugin {
+ private ConfigManager configManager;
+ private SyncListManager syncListManager;
+
+ @Override
+ public void onDisable() {
+ configManager.flush();
+ super.onDisable();
+ }
+
+ @Override
+ public void onLoad() {
+ configManager = new ConfigManager(new File("plugins", getName() + ".yml"), new JacksonYamlSerialiser<>(), getLogger());
+ configManager.load();
+ syncListManager = new SyncListManager(configManager.getConfiguration(), new ApacheCommonsIOSynchroniser(), getLogger());
+ configManager.addConfigLoadListener(syncListManager);
+ if (configManager.getConfiguration().getSyncOnLoad()) {
+ syncListManager.synchroniseAllSyncList();
+ }
+ super.onLoad();
+ }
+
+ @Override
+ public void onEnable() {
+ super.onEnable();
+ }
+
+ @Override
+ public FileConfiguration getConfig() {
+ return configManager;
+ }
+
+ @Override
+ public void saveConfig() {
+ configManager.flush();
+ }
+
+ @Override
+ public void saveDefaultConfig() {
+ configManager.resetConfiguration();
+ configManager.flush();
+ super.saveDefaultConfig();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/solutions/reslate/entertainment/spigotresourcesync/SyncListManager.java b/src/main/java/solutions/reslate/entertainment/spigotresourcesync/SyncListManager.java
new file mode 100644
index 0000000..b60ec28
--- /dev/null
+++ b/src/main/java/solutions/reslate/entertainment/spigotresourcesync/SyncListManager.java
@@ -0,0 +1,47 @@
+package solutions.reslate.entertainment.spigotresourcesync;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.logging.Logger;
+
+import solutions.reslate.entertainment.spigotresourcesync.data.Configuration;
+import solutions.reslate.entertainment.spigotresourcesync.data.SyncPair;
+import solutions.reslate.entertainment.spigotresourcesync.events.ObjectLoadListener;
+import solutions.reslate.entertainment.spigotresourcesync.synchronisation.Synchroniser;
+
+public class SyncListManager implements ObjectLoadListener {
+ private Configuration configuration;
+ private Synchroniser synchroniser;
+ private Logger logger;
+
+ public SyncListManager(Configuration configuration, Synchroniser synchroniser, Logger logger) {
+ super();
+ this.logger = logger;
+ this.configuration = configuration;
+ this.synchroniser = synchroniser;
+ }
+
+ public void synchroniseAllSyncList() {
+ logger.info("Synchronising all sync pairs...");
+ for (SyncPair syncPair : this.configuration.getSyncList().gatherAllSyncPairs()) {
+ File source = new File(syncPair.getSource());
+ File dest = new File(syncPair.getDestination());
+ try {
+ synchroniser.sync(source, dest);
+ logger.info(String.format("Synchronised \"%s\" to \"%s\"!", source.getName(), dest.getName()));
+ } catch (IOException e) {
+ logger.warning(String.format("Failed to synchronise \"%s\" to \"%s\". %s", source.getAbsolutePath(), dest.getAbsolutePath(), e.getMessage()));
+ }
+ }
+ logger.info("Done synchronising.");
+ }
+
+ @Override
+ public void objectLoaded(Configuration obj) {
+ logger.info("Updating synchronisation list due to recently loading configuration...");
+ this.configuration = obj;
+ synchroniseAllSyncList();
+ logger.info("Done.");
+ }
+
+}
diff --git a/src/main/java/solutions/reslate/entertainment/spigotresourcesync/data/Configuration.java b/src/main/java/solutions/reslate/entertainment/spigotresourcesync/data/Configuration.java
new file mode 100644
index 0000000..1c1f159
--- /dev/null
+++ b/src/main/java/solutions/reslate/entertainment/spigotresourcesync/data/Configuration.java
@@ -0,0 +1,42 @@
+package solutions.reslate.entertainment.spigotresourcesync.data;
+
+import java.io.Serializable;
+
+public class Configuration implements Serializable {
+ private boolean enabled;
+ private boolean syncOnLoad;
+ private SyncList syncList;
+
+ public Configuration() {
+ super();
+ enabled = true;
+ syncOnLoad = true;
+ syncList = new SyncList();
+ syncList.addSynchronisationPair("exampleA", "exampleB", "general");
+ }
+
+ public boolean getEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public boolean getSyncOnLoad() {
+ return syncOnLoad;
+ }
+
+ public void setSyncOnLoad(boolean syncOnLoad) {
+ this.syncOnLoad = syncOnLoad;
+ }
+
+ public SyncList getSyncList() {
+ return syncList;
+ }
+
+ public void setSyncList(SyncList syncList) {
+ this.syncList = syncList;
+ }
+
+}
diff --git a/src/main/java/solutions/reslate/entertainment/spigotresourcesync/data/SyncList.java b/src/main/java/solutions/reslate/entertainment/spigotresourcesync/data/SyncList.java
new file mode 100644
index 0000000..7bbbaea
--- /dev/null
+++ b/src/main/java/solutions/reslate/entertainment/spigotresourcesync/data/SyncList.java
@@ -0,0 +1,67 @@
+package solutions.reslate.entertainment.spigotresourcesync.data;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.SequencedCollection;
+import java.util.SequencedMap;
+
+
+public class SyncList implements Serializable {
+ private SequencedMap> syncPairs;
+
+ public SyncList() {
+ super();
+ this.syncPairs = new LinkedHashMap<>();
+ }
+
+ public List gatherAllSyncPairs() {
+ List allSyncPairs = new ArrayList<>();
+ for (SequencedCollection syncPairs : this.syncPairs.values()) {
+ allSyncPairs.addAll(syncPairs);
+ }
+ return allSyncPairs;
+ }
+
+ public void addSynchronisationPair(String source, String dest, String group) {
+ this.addSynchronisationPair(new SyncPair(source, dest), group);
+ }
+
+ public void addSynchronisationPair(SyncPair syncPair, String group) {
+ addSynchronisationGroup(group);
+ this.syncPairs.get(group).add(syncPair);
+ }
+
+ public void removeSynchronisationSet(String source, String dest, String group) {
+ if (this.syncPairs.containsKey(group)) {
+ this.syncPairs.get(group).remove(new SyncPair(source, dest));
+ }
+ }
+
+ public void addSynchronisationGroup(String group) {
+ if (!this.syncPairs.containsKey(group)) {
+ this.syncPairs.put(group, new LinkedHashSet<>());
+ }
+ }
+
+ public void removeSynchronisationGroup(String group) {
+ if (this.syncPairs.containsKey(group)) {
+ this.syncPairs.remove(group);
+ }
+ }
+
+ public List gatherSynchronisationGroups() {
+ return new ArrayList<>(this.syncPairs.keySet());
+ }
+
+ public SequencedMap> getSyncPairs() {
+ return syncPairs;
+ }
+
+ public void setSyncPairs(SequencedMap> syncPairs) {
+ this.syncPairs = syncPairs;
+ }
+
+}
diff --git a/src/main/java/solutions/reslate/entertainment/spigotresourcesync/data/SyncPair.java b/src/main/java/solutions/reslate/entertainment/spigotresourcesync/data/SyncPair.java
new file mode 100644
index 0000000..9269e34
--- /dev/null
+++ b/src/main/java/solutions/reslate/entertainment/spigotresourcesync/data/SyncPair.java
@@ -0,0 +1,55 @@
+package solutions.reslate.entertainment.spigotresourcesync.data;
+
+import java.io.Serializable;
+import java.util.Objects;
+
+public class SyncPair implements Serializable {
+ private String source;
+ private String destination;
+
+ public SyncPair(String source, String destination) {
+ super();
+ this.source = source;
+ this.destination = destination;
+ }
+
+ public SyncPair() {
+ super();
+ }
+
+ public void setSource(String source) {
+ this.source = source;
+ }
+
+ public void setDestination(String destination) {
+ this.destination = destination;
+ }
+
+ public String getSource() {
+ return source;
+ }
+
+ public String getDestination() {
+ return destination;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(this.source, this.destination);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ SyncPair syncPair = (SyncPair) obj;
+ return this.source.equals(syncPair.source)
+ && this.destination.equals(syncPair.destination);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("(%s, %s)", this.source, this.destination);
+ }
+}
diff --git a/src/main/java/solutions/reslate/entertainment/spigotresourcesync/events/ObjectLoadListener.java b/src/main/java/solutions/reslate/entertainment/spigotresourcesync/events/ObjectLoadListener.java
new file mode 100644
index 0000000..4f2bd70
--- /dev/null
+++ b/src/main/java/solutions/reslate/entertainment/spigotresourcesync/events/ObjectLoadListener.java
@@ -0,0 +1,7 @@
+package solutions.reslate.entertainment.spigotresourcesync.events;
+
+import java.util.EventListener;
+
+public interface ObjectLoadListener extends EventListener {
+ void objectLoaded(T obj);
+}
diff --git a/src/main/java/solutions/reslate/entertainment/spigotresourcesync/serialisation/JacksonYamlSerialiser.java b/src/main/java/solutions/reslate/entertainment/spigotresourcesync/serialisation/JacksonYamlSerialiser.java
new file mode 100644
index 0000000..e72653f
--- /dev/null
+++ b/src/main/java/solutions/reslate/entertainment/spigotresourcesync/serialisation/JacksonYamlSerialiser.java
@@ -0,0 +1,68 @@
+package solutions.reslate.entertainment.spigotresourcesync.serialisation;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.Reader;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator.Feature;
+
+public class JacksonYamlSerialiser implements Serialiser {
+
+ private ObjectMapper mapper;
+
+ public JacksonYamlSerialiser() {
+ super();
+ YAMLFactory yamlFactory = new YAMLFactory()
+ .disable(Feature.WRITE_DOC_START_MARKER);
+ mapper = new ObjectMapper(yamlFactory)
+ .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
+ }
+
+ @Override
+ public void flushTo(File file, T serialisable) throws IOException {
+ try (FileWriter writer = new FileWriter(file)) {
+ mapper.writeValue(writer, serialisable);
+ }
+ }
+
+ @Override
+ public T load(File file, Class type) throws IOException {
+ try (FileReader reader = new FileReader(file)) {
+ return this.load(reader, type);
+ }
+ }
+
+ @Override
+ public T load(Reader reader, Class type) throws IOException {
+ try {
+ return mapper.readValue(reader, type);
+ } catch (IOException e) {
+ throw e;
+ }
+ }
+
+ @Override
+ public String serialize(T serialisable) {
+ try {
+ return mapper.writeValueAsString(serialisable);
+ } catch (JsonProcessingException e) {
+ throw new IllegalArgumentException("Given type cannot be serialised.", e);
+ }
+ }
+
+ @Override
+ public T deserialize(String data, Class type) {
+ try {
+ return mapper.readValue(data, type);
+ } catch (JsonProcessingException e) {
+ throw new IllegalArgumentException("Given string cannot be deserialised.", e);
+ }
+ }
+
+}
diff --git a/src/main/java/solutions/reslate/entertainment/spigotresourcesync/serialisation/Serialiser.java b/src/main/java/solutions/reslate/entertainment/spigotresourcesync/serialisation/Serialiser.java
new file mode 100644
index 0000000..1309102
--- /dev/null
+++ b/src/main/java/solutions/reslate/entertainment/spigotresourcesync/serialisation/Serialiser.java
@@ -0,0 +1,14 @@
+package solutions.reslate.entertainment.spigotresourcesync.serialisation;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.Reader;
+
+public interface Serialiser {
+ void flushTo(File file, T serialisable) throws IOException;
+ String serialize(T serialisable);
+ T load(File file, Class type) throws IOException;
+ T load(Reader reader, Class type) throws IOException;
+ T deserialize(String data, Class type);
+
+}
diff --git a/src/main/java/solutions/reslate/entertainment/spigotresourcesync/synchronisation/ApacheCommonsIOSynchroniser.java b/src/main/java/solutions/reslate/entertainment/spigotresourcesync/synchronisation/ApacheCommonsIOSynchroniser.java
new file mode 100644
index 0000000..36f0d62
--- /dev/null
+++ b/src/main/java/solutions/reslate/entertainment/spigotresourcesync/synchronisation/ApacheCommonsIOSynchroniser.java
@@ -0,0 +1,49 @@
+package solutions.reslate.entertainment.spigotresourcesync.synchronisation;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+
+import org.apache.commons.io.FileUtils;
+
+public class ApacheCommonsIOSynchroniser implements Synchroniser {
+
+ @Override
+ public void sync(File source, File dest, File root) throws IOException {
+ if (root != null) {
+ source = Path.of(root.getPath(), source.getPath()).toFile();
+ source = Path.of(root.getPath(), dest.getPath()).toFile();
+ }
+
+ if (source.isDirectory()) {
+ if (dest.isDirectory()) {
+ FileUtils.copyDirectory(source, dest);
+ } else {
+ throw new IOException("Cannot synchronise directy to non-directory!");
+ // TODO add more detail to error message.
+ }
+ } else {
+ if (dest.isDirectory()) {
+ FileUtils.copyFileToDirectory(source, dest);
+ } else {
+ FileUtils.copyFile(source, dest);
+ }
+ }
+ }
+
+ @Override
+ public void sync(String source, String dest, String root) throws IOException {
+ this.sync(new File(source), new File(dest), root != null ? new File(root) : null);
+ }
+
+ @Override
+ public void sync(File source, File dest) throws IOException {
+ sync(source, dest, null);
+ }
+
+ @Override
+ public void sync(String source, String dest) throws IOException {
+ sync(source, dest, null);
+ }
+
+}
diff --git a/src/main/java/solutions/reslate/entertainment/spigotresourcesync/synchronisation/Synchroniser.java b/src/main/java/solutions/reslate/entertainment/spigotresourcesync/synchronisation/Synchroniser.java
new file mode 100644
index 0000000..119ab74
--- /dev/null
+++ b/src/main/java/solutions/reslate/entertainment/spigotresourcesync/synchronisation/Synchroniser.java
@@ -0,0 +1,11 @@
+package solutions.reslate.entertainment.spigotresourcesync.synchronisation;
+
+import java.io.File;
+import java.io.IOException;
+
+public interface Synchroniser {
+ void sync(File source, File dest, File root) throws IOException;
+ void sync(File source, File dest) throws IOException;
+ void sync(String source, String dest, String root) throws IOException;
+ void sync(String source, String dest) throws IOException;
+}
diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml
new file mode 100644
index 0000000..5f182e2
--- /dev/null
+++ b/src/main/resources/plugin.yml
@@ -0,0 +1,3 @@
+name: SpigotResourceSync
+version: 1.0.0
+main: solutions.reslate.entertainment.spigotresourcesync.SpigotResourceSync
\ No newline at end of file
diff --git a/src/test/java/solutions/reslate/entertainment/spigotresourcesync/TestConfigManager.java b/src/test/java/solutions/reslate/entertainment/spigotresourcesync/TestConfigManager.java
new file mode 100644
index 0000000..8ff6163
--- /dev/null
+++ b/src/test/java/solutions/reslate/entertainment/spigotresourcesync/TestConfigManager.java
@@ -0,0 +1,32 @@
+package solutions.reslate.entertainment.spigotresourcesync;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.logging.Logger;
+
+import org.junit.jupiter.api.Test;
+
+import solutions.reslate.entertainment.spigotresourcesync.data.Configuration;
+import solutions.reslate.entertainment.spigotresourcesync.data.SyncPair;
+import solutions.reslate.entertainment.spigotresourcesync.serialisation.JacksonYamlSerialiser;
+import solutions.reslate.entertainment.spigotresourcesync.serialisation.Serialiser;
+
+public class TestConfigManager {
+ @Test
+ void persistConfiguration() throws IOException {
+ File dummyConfig = File.createTempFile("dummy_config", ".yml");
+ Serialiser dummySerialiser = new JacksonYamlSerialiser<>();
+ Logger logger = Logger.getLogger(this.getClass().getName());
+ ConfigManager initialConfigManager = new ConfigManager(dummyConfig, dummySerialiser, logger);
+ SyncPair dummySyncPair = new SyncPair("abc", "def");
+ initialConfigManager.getConfiguration().getSyncList().addSynchronisationPair(dummySyncPair, "a");
+ initialConfigManager.flush();
+ assumeTrue(dummyConfig.exists());
+ ConfigManager finalConfigManager = new ConfigManager(dummyConfig, dummySerialiser, logger);
+ finalConfigManager.load();
+ assertEquals(dummySyncPair, finalConfigManager.getConfiguration().getSyncList().gatherAllSyncPairs().get(0));
+ }
+}
diff --git a/src/test/java/solutions/reslate/entertainment/spigotresourcesync/TestSyncListManager.java b/src/test/java/solutions/reslate/entertainment/spigotresourcesync/TestSyncListManager.java
new file mode 100644
index 0000000..17d3b8c
--- /dev/null
+++ b/src/test/java/solutions/reslate/entertainment/spigotresourcesync/TestSyncListManager.java
@@ -0,0 +1,10 @@
+package solutions.reslate.entertainment.spigotresourcesync;
+
+import org.junit.jupiter.api.Test;
+
+public class TestSyncListManager {
+ @Test
+ void testDirectorySync() {
+ // TODO Write directory sync test.
+ }
+}
diff --git a/src/test/java/solutions/reslate/entertainment/spigotresourcesync/data/TestSynchronisationPair.java b/src/test/java/solutions/reslate/entertainment/spigotresourcesync/data/TestSynchronisationPair.java
new file mode 100644
index 0000000..aa544ab
--- /dev/null
+++ b/src/test/java/solutions/reslate/entertainment/spigotresourcesync/data/TestSynchronisationPair.java
@@ -0,0 +1,14 @@
+package solutions.reslate.entertainment.spigotresourcesync.data;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+
+public class TestSynchronisationPair {
+ @Test
+ void dataEqualsThereforeObjectEquals() {
+ SyncPair firstPair = new SyncPair("abc", "def");
+ SyncPair secondPair = new SyncPair("abc", "def");
+ assertEquals(firstPair, secondPair);
+ }
+}
diff --git a/src/test/java/solutions/reslate/entertainment/spigotresourcesync/serialisation/TestJacksonYamlSerialiser.java b/src/test/java/solutions/reslate/entertainment/spigotresourcesync/serialisation/TestJacksonYamlSerialiser.java
new file mode 100644
index 0000000..b05b9b4
--- /dev/null
+++ b/src/test/java/solutions/reslate/entertainment/spigotresourcesync/serialisation/TestJacksonYamlSerialiser.java
@@ -0,0 +1,78 @@
+package solutions.reslate.entertainment.spigotresourcesync.serialisation;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.SequencedCollection;
+import java.util.SequencedMap;
+
+import org.junit.jupiter.api.Test;
+
+import solutions.reslate.entertainment.spigotresourcesync.data.SyncPair;
+
+public class TestJacksonYamlSerialiser {
+ @Test
+ void testListSerialisation() throws IOException {
+ ArrayList dummyList = new ArrayList<>();
+ dummyList.add("abc");
+ dummyList.add("def");
+ File dummyFile = File.createTempFile("dummy_file", ".yml");
+ JacksonYamlSerialiser> dummyJacksonYamlSerialiser = new JacksonYamlSerialiser<>();
+ dummyJacksonYamlSerialiser.flushTo(dummyFile, dummyList);
+ assumeTrue(dummyFile.exists());
+ List writtenLines = Files.readAllLines(dummyFile.toPath());
+ assertEquals(2, writtenLines.size());
+ assertEquals("- \"abc\"", writtenLines.get(0));
+ assertEquals("- \"def\"", writtenLines.get(1));
+ }
+
+ @Test
+ void testSyncPairSerialisation() throws IOException {
+ File dummyFile = File.createTempFile("dummy_file", ".yml");
+ SyncPair dummySyncPair = new SyncPair("abc", "def");
+ JacksonYamlSerialiser dummyJacksonYamlSerialiser = new JacksonYamlSerialiser<>();
+ dummyJacksonYamlSerialiser.flushTo(dummyFile, dummySyncPair);
+ assumeTrue(dummyFile.exists());
+ List writtenLines = Files.readAllLines(dummyFile.toPath());
+ assertEquals(2, writtenLines.size());
+ assertEquals("source: \"abc\"", writtenLines.get(0));
+ assertEquals("destination: \"def\"", writtenLines.get(1));
+ }
+
+ @Test
+ void testSequencedCollectionSerialisation() throws IOException {
+ File dummyFile = File.createTempFile("dummy_file", ".yml");
+ SequencedCollection collection = new LinkedHashSet<>();
+ collection.add("abc");
+ collection.add("def");
+ JacksonYamlSerialiser> dummyJacksonYamlSerialiser = new JacksonYamlSerialiser<>();
+ dummyJacksonYamlSerialiser.flushTo(dummyFile, collection);
+ assumeTrue(dummyFile.exists());
+ List writtenLines = Files.readAllLines(dummyFile.toPath());
+ assertEquals(2, writtenLines.size());
+ assertEquals("- \"abc\"", writtenLines.get(0));
+ assertEquals("- \"def\"", writtenLines.get(1));
+ }
+
+ @Test
+ void testSequencedMapSerialisation() throws IOException {
+ File dummyFile = File.createTempFile("dummy_file", ".yml");
+ SequencedMap collection = new LinkedHashMap<>();
+ collection.put("abc", "def");
+ collection.put("123", "456");
+ JacksonYamlSerialiser> dummyJacksonYamlSerialiser = new JacksonYamlSerialiser<>();
+ dummyJacksonYamlSerialiser.flushTo(dummyFile, collection);
+ assumeTrue(dummyFile.exists());
+ List writtenLines = Files.readAllLines(dummyFile.toPath());
+ assertEquals(2, writtenLines.size());
+ assertEquals("abc: \"def\"", writtenLines.get(0));
+ assertEquals("\"123\": \"456\"", writtenLines.get(1));
+ }
+}
diff --git a/src/test/java/solutions/reslate/entertainment/spigotresourcesync/synchroniser/TestApacheCommonsIOSynchroniser.java b/src/test/java/solutions/reslate/entertainment/spigotresourcesync/synchroniser/TestApacheCommonsIOSynchroniser.java
new file mode 100644
index 0000000..59f0e73
--- /dev/null
+++ b/src/test/java/solutions/reslate/entertainment/spigotresourcesync/synchroniser/TestApacheCommonsIOSynchroniser.java
@@ -0,0 +1,44 @@
+package solutions.reslate.entertainment.spigotresourcesync.synchroniser;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+
+import org.junit.jupiter.api.Test;
+
+import solutions.reslate.entertainment.spigotresourcesync.synchronisation.ApacheCommonsIOSynchroniser;
+
+public class TestApacheCommonsIOSynchroniser {
+ @Test
+ void testDir2DirSync() throws IOException {
+ ApacheCommonsIOSynchroniser dummySynchroniser = new ApacheCommonsIOSynchroniser();
+ File dummyDirA = Files.createTempDirectory("dir_A").toFile();
+ File dummyDirB = Files.createTempDirectory("dir_B").toFile();
+ File dummyFileA = File.createTempFile("file_A", ".txt", dummyDirA);
+ dummySynchroniser.sync(dummyDirA, dummyDirB);
+ assertTrue(new File(dummyDirB, dummyFileA.getName()).exists());
+ }
+
+ @Test
+ void testFile2DirSync() throws IOException {
+ ApacheCommonsIOSynchroniser dummySynchroniser = new ApacheCommonsIOSynchroniser();
+ File dummyDirA = Files.createTempDirectory("dir_A").toFile();
+ File dummyDirB = Files.createTempDirectory("dir_B").toFile();
+ File dummyFileA = File.createTempFile("file_A", ".txt", dummyDirA);
+ dummySynchroniser.sync(dummyFileA, dummyDirB);
+ assertTrue(new File(dummyDirB, dummyFileA.getName()).exists());
+ }
+
+ @Test
+ void testFile2FileSync() throws IOException {
+ ApacheCommonsIOSynchroniser dummySynchroniser = new ApacheCommonsIOSynchroniser();
+ File dummyDirA = Files.createTempDirectory("dir_A").toFile();
+ File dummyDirB = Files.createTempDirectory("dir_B").toFile();
+ File dummyFileA = File.createTempFile("file_A", ".txt", dummyDirA);
+ File dummyFileB = new File(dummyDirB, "file_B");
+ dummySynchroniser.sync(dummyFileA, dummyFileB);
+ assertTrue(dummyFileB.exists());
+ }
+}