diff --git a/src/main/java/nl/andrewlalis/crystalkeep/CrystalKeep.java b/src/main/java/nl/andrewlalis/crystalkeep/CrystalKeep.java index e13a7e4..8e63300 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/CrystalKeep.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/CrystalKeep.java @@ -1,7 +1,60 @@ package nl.andrewlalis.crystalkeep; -public class CrystalKeep { - public static void main(String[] args) { - System.out.println("Hello world!"); +import javafx.application.Application; +import javafx.fxml.FXMLLoader; +import javafx.scene.Scene; +import javafx.stage.Stage; +import nl.andrewlalis.crystalkeep.model.Cluster; +import nl.andrewlalis.crystalkeep.model.Shard; +import nl.andrewlalis.crystalkeep.model.serialization.ClusterSerializer; +import nl.andrewlalis.crystalkeep.model.shards.LoginCredentialsShard; +import nl.andrewlalis.crystalkeep.model.shards.TextShard; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.URL; +import java.time.LocalDateTime; +import java.util.Arrays; + +public class CrystalKeep extends Application { + public static void main(String[] args) throws IOException { + launch(args); + } + + @Override + public void start(Stage stage) throws Exception { + URL url = CrystalKeep.class.getClassLoader().getResource("ui/crystalkeep.fxml"); + FXMLLoader loader = new FXMLLoader(url); + var scene = new Scene(loader.load()); + stage.setScene(scene); + stage.setTitle("CrystalKeep"); + stage.sizeToScene(); + stage.show(); + } + + public static void test() throws IOException { + Cluster c = new Cluster("Test"); + Shard s = new TextShard(c, "sample", LocalDateTime.now(), "Hello world!"); + Shard s2 = new LoginCredentialsShard(c, "logs", LocalDateTime.now().plusHours(3), "andrew", "testing"); + c.addShard(s); + c.addShard(s2); + Cluster c2 = new Cluster("Test2"); + Shard s3 = new TextShard(c2, "another sample", LocalDateTime.now().plusMinutes(3), "Testing this stuff...."); + c2.addShard(s3); + Cluster parent = new Cluster("Parent"); + parent.addCluster(c); + parent.addCluster(c2); + + System.out.println(parent); + long start = System.currentTimeMillis(); + byte[] cBytes = ClusterSerializer.toBytes(parent); + long dur = System.currentTimeMillis() - start; + System.out.println("Duration: " + dur + " milliseconds"); + System.out.println(Arrays.toString(cBytes)); + + Cluster cLoaded = ClusterSerializer.clusterFromBytes(new ByteArrayInputStream(cBytes), null); + System.out.println(cLoaded); + System.out.println(Arrays.toString(ClusterSerializer.toBytes(cLoaded))); + System.out.println(Arrays.equals(cBytes, ClusterSerializer.toBytes(cLoaded))); } } diff --git a/src/main/java/nl/andrewlalis/crystalkeep/model/Cluster.java b/src/main/java/nl/andrewlalis/crystalkeep/model/Cluster.java index ccba5d4..d51248a 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/model/Cluster.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/model/Cluster.java @@ -1,41 +1,71 @@ package nl.andrewlalis.crystalkeep.model; import lombok.Getter; +import lombok.Setter; -import java.io.ByteArrayOutputStream; import java.util.HashSet; +import java.util.Objects; import java.util.Set; @Getter -public class Cluster { +public class Cluster implements Comparable { + @Setter + private String name; private final Set shards; private final Set clusters; - private final Cluster parent; + @Setter + private Cluster parent; - public Cluster(Set shards, Set clusters, Cluster parent) { + public Cluster(String name, Set shards, Set clusters, Cluster parent) { + this.name = name; this.shards = shards; this.clusters = clusters; this.parent = parent; - if (this.parent != null) { - this.parent.addCluster(this); - } } - public Cluster() { - this(new HashSet<>(), new HashSet<>(), null); + public Cluster(String name) { + this(name, new HashSet<>(), new HashSet<>(), null); } public void addShard(Shard shard) { - if (!this.equals(shard.getCluster())) { - throw new IllegalArgumentException("Shard must have correct cluster set before adding it to shard."); - } this.shards.add(shard); + shard.setCluster(this); } public void addCluster(Cluster cluster) { - if (!this.equals(cluster.getParent())) { - throw new IllegalArgumentException("Cluster must have correct parent set before adding it to cluster."); - } this.clusters.add(cluster); + cluster.setParent(this); + } + + @Override + public int compareTo(Cluster o) { + return this.getName().compareTo(o.getName()); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Cluster cluster = (Cluster) o; + return getName().equals(cluster.getName()) && Objects.equals(getParent(), cluster.getParent()); + } + + @Override + public int hashCode() { + return Objects.hash(getName(), getParent()); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("Cluster: name=\"").append(this.getName()).append("\"\nShards:\n"); + for (Shard s : this.getShards()) { + sb.append('\t').append(s.toString()).append('\n'); + } + sb.append("Nested clusters:\n"); + for (Cluster c : this.getClusters()) { + sb.append('\n').append(c.toString()).append('\n'); + } + return sb.toString(); } } diff --git a/src/main/java/nl/andrewlalis/crystalkeep/model/ClusterSerializer.java b/src/main/java/nl/andrewlalis/crystalkeep/model/ClusterSerializer.java deleted file mode 100644 index e311c1d..0000000 --- a/src/main/java/nl/andrewlalis/crystalkeep/model/ClusterSerializer.java +++ /dev/null @@ -1,44 +0,0 @@ -package nl.andrewlalis.crystalkeep.model; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.HashSet; - -public class ClusterSerializer { - public byte[] toBytes(Cluster cluster) throws IOException { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - bos.write(this.toBytes(cluster.getClusters().size())); - for (Cluster child : cluster.getClusters()) { - bos.write(this.toBytes(child)); - } - bos.write(this.toBytes(cluster.getShards().size())); - for (Shard shard : cluster.getShards()) { - bos.write(shard.toBytes()); - } - return bos.toByteArray(); - } - - public Cluster fromBytes(ByteArrayInputStream bis, Cluster parent) throws IOException { - Cluster cluster = new Cluster(new HashSet<>(), new HashSet<>(), parent); - int childCount = this.toInt(bis.readNBytes(4)); - for (int i = 0; i < childCount; i++) { - cluster.addCluster(this.fromBytes(bis, cluster)); - } - int shardCount = this.toInt(bis.readNBytes(4)); - for (int i = 0; i < shardCount; i++) { - cluster.addShard(Shard.fromBytes(bis)); - } - return cluster; - } - - public byte[] toBytes(int x) { - return ByteBuffer.allocate(4).putInt(x).array(); - } - - public int toInt(byte[] bytes) { - assert(bytes.length == 4); - return ByteBuffer.wrap(bytes).getInt(); - } -} diff --git a/src/main/java/nl/andrewlalis/crystalkeep/model/Shard.java b/src/main/java/nl/andrewlalis/crystalkeep/model/Shard.java index d4db489..475b358 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/model/Shard.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/model/Shard.java @@ -2,22 +2,35 @@ package nl.andrewlalis.crystalkeep.model; import lombok.Getter; import lombok.Setter; +import nl.andrewlalis.crystalkeep.model.shards.ShardType; -import java.io.ByteArrayInputStream; import java.time.LocalDateTime; +import java.util.Objects; +/** + * A shard is a single "piece" of information, such as a snippet of text, login + * credentials, an image, or a private key. All shards within a cluster should + * have unique names. + *

+ * Due to the need to deserialize shards from byte arrays, it is required + * that this parent class holds a type discriminator value, which is used to + * decide which shard type to deserialize. + *

+ */ @Getter public abstract class Shard implements Comparable { - private final Cluster cluster; + @Setter + private Cluster cluster; @Setter private String name; private final LocalDateTime createdAt; + private final ShardType type; - public Shard(Cluster cluster, String name, LocalDateTime createdAt) { + public Shard(Cluster cluster, String name, LocalDateTime createdAt, ShardType type) { this.cluster = cluster; - this.cluster.addShard(this); this.name = name; this.createdAt = createdAt; + this.type = type; } @Override @@ -27,11 +40,21 @@ public abstract class Shard implements Comparable { return this.getCreatedAt().compareTo(o.getCreatedAt()); } - public byte[] toBytes() { - return new byte[0]; + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Shard shard = (Shard) o; + return getCluster().equals(shard.getCluster()) && getName().equals(shard.getName()) && getType() == shard.getType(); } - public static Shard fromBytes(ByteArrayInputStream bis) { - return null; + @Override + public int hashCode() { + return Objects.hash(getCluster(), getName(), getType()); + } + + @Override + public String toString() { + return "Shard: name=\"" + this.name + "\", type=" + this.type + ", createdAt=" + this.createdAt; } } diff --git a/src/main/java/nl/andrewlalis/crystalkeep/model/serialization/ByteUtils.java b/src/main/java/nl/andrewlalis/crystalkeep/model/serialization/ByteUtils.java new file mode 100644 index 0000000..397a1a2 --- /dev/null +++ b/src/main/java/nl/andrewlalis/crystalkeep/model/serialization/ByteUtils.java @@ -0,0 +1,45 @@ +package nl.andrewlalis.crystalkeep.model.serialization; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +public class ByteUtils { + public static byte[] toBytes(int x) { + return ByteBuffer.allocate(4).putInt(x).array(); + } + + public static int toInt(byte[] bytes) { + assert(bytes.length == 4); + return ByteBuffer.wrap(bytes).getInt(); + } + + public static void writeLengthPrefixed(byte[] bytes, OutputStream os) throws IOException { + os.write(toBytes(bytes.length)); + os.write(bytes); + } + + public static void writeLengthPrefixed(String s, OutputStream os) throws IOException { + writeLengthPrefixed(s.getBytes(StandardCharsets.UTF_8), os); + } + + public static byte[] writeLengthPrefixedStrings(String[] strings) throws IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + for (String s : strings) { + writeLengthPrefixed(s, bos); + } + return bos.toByteArray(); + } + + public static byte[] readLengthPrefixed(InputStream is) throws IOException { + int count = toInt(is.readNBytes(4)); + return is.readNBytes(count); + } + + public static String readLengthPrefixedString(InputStream is) throws IOException { + return new String(readLengthPrefixed(is), StandardCharsets.UTF_8); + } +} diff --git a/src/main/java/nl/andrewlalis/crystalkeep/model/serialization/ClusterSerializer.java b/src/main/java/nl/andrewlalis/crystalkeep/model/serialization/ClusterSerializer.java new file mode 100644 index 0000000..3c53750 --- /dev/null +++ b/src/main/java/nl/andrewlalis/crystalkeep/model/serialization/ClusterSerializer.java @@ -0,0 +1,116 @@ +package nl.andrewlalis.crystalkeep.model.serialization; + +import nl.andrewlalis.crystalkeep.model.Cluster; +import nl.andrewlalis.crystalkeep.model.Shard; +import nl.andrewlalis.crystalkeep.model.shards.LoginCredentialsShard; +import nl.andrewlalis.crystalkeep.model.shards.ShardType; +import nl.andrewlalis.crystalkeep.model.shards.TextShard; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; + +import static nl.andrewlalis.crystalkeep.model.serialization.ByteUtils.toInt; + +public class ClusterSerializer { + private static final Map> serializers = new HashMap<>(); + static { + serializers.put(ShardType.TEXT, new TextShard.Serializer()); + serializers.put(ShardType.LOGIN_CREDENTIALS, new LoginCredentialsShard.Serializer()); + } + + /** + * Serializes a cluster to a byte array, including all shards and nested + * clusters. + * @param cluster The cluster to serialize. + * @return The byte array representing the cluster. + * @throws IOException If an error occurs while writing the cluster. + */ + public static byte[] toBytes(Cluster cluster) throws IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ByteUtils.writeLengthPrefixed(cluster.getName(), bos); + + bos.write(ByteUtils.toBytes(cluster.getClusters().size())); + Cluster[] children = new Cluster[cluster.getClusters().size()]; + cluster.getClusters().toArray(children); + Arrays.sort(children); + for (Cluster child : children) { + bos.write(toBytes(child)); + } + + bos.write(ByteUtils.toBytes(cluster.getShards().size())); + Shard[] shards = new Shard[cluster.getShards().size()]; + cluster.getShards().toArray(shards); + Arrays.sort(shards); + for (Shard shard : shards) { + bos.write(toBytes(shard)); + } + return bos.toByteArray(); + } + + /** + * Reads a cluster from an input stream. + * @param is The input stream to read from. + * @param parent The parent cluster of this cluster. This may be null. + * @return The cluster that was read. + * @throws IOException If data could not be read from the stream. + */ + public static Cluster clusterFromBytes(InputStream is, Cluster parent) throws IOException { + String name = ByteUtils.readLengthPrefixedString(is); + Cluster cluster = new Cluster(name, new HashSet<>(), new HashSet<>(), parent); + int childCount = toInt(is.readNBytes(4)); + for (int i = 0; i < childCount; i++) { + cluster.addCluster(clusterFromBytes(is, cluster)); + } + int shardCount = toInt(is.readNBytes(4)); + for (int i = 0; i < shardCount; i++) { + cluster.addShard(shardFromBytes(is, cluster)); + } + return cluster; + } + + /** + * Serializes a shard to a byte array. It does this by first writing the + * standard shard information, then using a specific {@link ShardSerializer} + * to get the bytes that represent the body of the shard. + * @param shard The shard to serialize. + * @return A byte array representing the shard. + * @throws IOException If byte array stream could not be written to. + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + public static byte[] toBytes(Shard shard) throws IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ByteUtils.writeLengthPrefixed(shard.getName().getBytes(StandardCharsets.UTF_8), bos); + ByteUtils.writeLengthPrefixed(shard.getCreatedAt().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME).getBytes(StandardCharsets.UTF_8), bos); + bos.write(ByteUtils.toBytes(shard.getType().getValue())); + ShardSerializer serializer = serializers.get(shard.getType()); + bos.write(serializer.serialize(shard)); + return bos.toByteArray(); + } + + /** + * Deserializes a shard from a byte array that's being read by the given + * input stream. + * @param is The input stream to read from. + * @param cluster The cluster that the shard should belong to. + * @return The shard that was deserialized. + * @throws IOException If an error occurs while reading bytes. + */ + public static Shard shardFromBytes(InputStream is, Cluster cluster) throws IOException { + String name = ByteUtils.readLengthPrefixedString(is); + LocalDateTime createdAt = LocalDateTime.parse(ByteUtils.readLengthPrefixedString(is), DateTimeFormatter.ISO_LOCAL_DATE_TIME); + ShardType type = ShardType.valueOf(ByteUtils.toInt(is.readNBytes(4))); + ShardSerializer serializer = serializers.get(type); + if (serializer == null) { + throw new IOException("Unsupported shard type."); + } + return serializer.deserialize(is, cluster, name, createdAt); + } +} diff --git a/src/main/java/nl/andrewlalis/crystalkeep/model/serialization/ShardSerializer.java b/src/main/java/nl/andrewlalis/crystalkeep/model/serialization/ShardSerializer.java new file mode 100644 index 0000000..3ce2b85 --- /dev/null +++ b/src/main/java/nl/andrewlalis/crystalkeep/model/serialization/ShardSerializer.java @@ -0,0 +1,15 @@ +package nl.andrewlalis.crystalkeep.model.serialization; + +import nl.andrewlalis.crystalkeep.model.Cluster; +import nl.andrewlalis.crystalkeep.model.Shard; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.time.LocalDateTime; + +public interface ShardSerializer { + byte[] serialize(T shard) throws IOException; + + T deserialize(InputStream bis, Cluster cluster, String name, LocalDateTime createdAt) throws IOException; +} diff --git a/src/main/java/nl/andrewlalis/crystalkeep/model/shards/LoginCredentialsShard.java b/src/main/java/nl/andrewlalis/crystalkeep/model/shards/LoginCredentialsShard.java index afba973..e433948 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/model/shards/LoginCredentialsShard.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/model/shards/LoginCredentialsShard.java @@ -4,7 +4,11 @@ import lombok.Getter; import lombok.Setter; import nl.andrewlalis.crystalkeep.model.Cluster; import nl.andrewlalis.crystalkeep.model.Shard; +import nl.andrewlalis.crystalkeep.model.serialization.ByteUtils; +import nl.andrewlalis.crystalkeep.model.serialization.ShardSerializer; +import java.io.IOException; +import java.io.InputStream; import java.time.LocalDateTime; @Getter @@ -14,8 +18,28 @@ public class LoginCredentialsShard extends Shard { private String password; public LoginCredentialsShard(Cluster cluster, String name, LocalDateTime createdAt, String username, String password) { - super(cluster, name, createdAt); + super(cluster, name, createdAt, ShardType.LOGIN_CREDENTIALS); this.username = username; this.password = password; } + + @Override + public String toString() { + return super.toString() + ", username=\"" + this.username + "\", password=\"" + this.password + "\""; + } + + public static class Serializer implements ShardSerializer { + + @Override + public byte[] serialize(LoginCredentialsShard shard) throws IOException { + return ByteUtils.writeLengthPrefixedStrings(new String[]{shard.getUsername(), shard.getPassword()}); + } + + @Override + public LoginCredentialsShard deserialize(InputStream is, Cluster cluster, String name, LocalDateTime createdAt) throws IOException { + String username = ByteUtils.readLengthPrefixedString(is); + String password = ByteUtils.readLengthPrefixedString(is); + return new LoginCredentialsShard(cluster, name, createdAt, username, password); + } + } } diff --git a/src/main/java/nl/andrewlalis/crystalkeep/model/shards/ShardType.java b/src/main/java/nl/andrewlalis/crystalkeep/model/shards/ShardType.java new file mode 100644 index 0000000..6ec3a4b --- /dev/null +++ b/src/main/java/nl/andrewlalis/crystalkeep/model/shards/ShardType.java @@ -0,0 +1,38 @@ +package nl.andrewlalis.crystalkeep.model.shards; + +import lombok.Getter; +import nl.andrewlalis.crystalkeep.model.Shard; + +/** + * Represents a distinct type of shard, and should correspond to exactly one + * subtype of {@link Shard}. + *

+ * All types defined here should have a unique integer value, which is used + * for serialization and deserialization of shards. + *

+ */ +public enum ShardType { + /** + * Represents a {@link TextShard} + */ + TEXT(1), + + /** + * Represents a {@link LoginCredentialsShard} + */ + LOGIN_CREDENTIALS(2); + + @Getter + private final int value; + + ShardType(int value) { + this.value = value; + } + + public static ShardType valueOf(int value) { + for (var type : values()) { + if (type.getValue() == value) return type; + } + throw new IllegalArgumentException("Invalid value."); + } +} diff --git a/src/main/java/nl/andrewlalis/crystalkeep/model/shards/TextShard.java b/src/main/java/nl/andrewlalis/crystalkeep/model/shards/TextShard.java index c2d35b6..6fade21 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/model/shards/TextShard.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/model/shards/TextShard.java @@ -1,15 +1,39 @@ package nl.andrewlalis.crystalkeep.model.shards; +import lombok.Getter; import nl.andrewlalis.crystalkeep.model.Cluster; import nl.andrewlalis.crystalkeep.model.Shard; +import nl.andrewlalis.crystalkeep.model.serialization.ByteUtils; +import nl.andrewlalis.crystalkeep.model.serialization.ShardSerializer; +import java.io.IOException; +import java.io.InputStream; import java.time.LocalDateTime; +@Getter public class TextShard extends Shard { private String text; public TextShard(Cluster cluster, String name, LocalDateTime createdAt, String text) { - super(cluster, name, createdAt); + super(cluster, name, createdAt, ShardType.TEXT); this.text = text; } + + @Override + public String toString() { + return super.toString() + ", text=\"" + this.text + "\""; + } + + public static class Serializer implements ShardSerializer { + @Override + public byte[] serialize(TextShard shard) throws IOException { + return ByteUtils.writeLengthPrefixedStrings(new String[]{shard.getText()}); + } + + @Override + public TextShard deserialize(InputStream is, Cluster cluster, String name, LocalDateTime createdAt) throws IOException { + String text = ByteUtils.readLengthPrefixedString(is); + return new TextShard(cluster, name, createdAt, text); + } + } } diff --git a/src/main/resources/crystalkeep.fxml b/src/main/resources/ui/crystalkeep.fxml similarity index 56% rename from src/main/resources/crystalkeep.fxml rename to src/main/resources/ui/crystalkeep.fxml index 2f5efbf..39352a8 100644 --- a/src/main/resources/crystalkeep.fxml +++ b/src/main/resources/ui/crystalkeep.fxml @@ -3,14 +3,8 @@ - -
- - - - - - -
+ + +