Added working serialization and starter javafx framework.

This commit is contained in:
Andrew Lalis 2021-05-28 19:49:33 +02:00
parent 00c343e673
commit 7bd89c0e50
11 changed files with 399 additions and 81 deletions

View File

@ -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)));
}
}

View File

@ -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<Cluster> {
@Setter
private String name;
private final Set<Shard> shards;
private final Set<Cluster> clusters;
private final Cluster parent;
@Setter
private Cluster parent;
public Cluster(Set<Shard> shards, Set<Cluster> clusters, Cluster parent) {
public Cluster(String name, Set<Shard> shards, Set<Cluster> 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();
}
}

View File

@ -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();
}
}

View File

@ -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.
* <p>
* 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.
* </p>
*/
@Getter
public abstract class Shard implements Comparable<Shard> {
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<Shard> {
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;
}
}

View File

@ -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);
}
}

View File

@ -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<ShardType, ShardSerializer<?>> 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);
}
}

View File

@ -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<T extends Shard> {
byte[] serialize(T shard) throws IOException;
T deserialize(InputStream bis, Cluster cluster, String name, LocalDateTime createdAt) throws IOException;
}

View File

@ -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<LoginCredentialsShard> {
@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);
}
}
}

View File

@ -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}.
* <p>
* All types defined here should have a unique integer value, which is used
* for serialization and deserialization of shards.
* </p>
*/
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.");
}
}

View File

@ -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<TextShard> {
@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);
}
}
}

View File

@ -3,14 +3,8 @@
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<BorderPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1">
<center>
<TabPane prefHeight="200.0" prefWidth="200.0" tabClosingPolicy="UNAVAILABLE" BorderPane.alignment="CENTER">
<tabs>
<Tab text="Shards" />
<Tab text="Untitled Tab 2" />
</tabs>
</TabPane>
</center>
<left>
<TreeView prefHeight="200.0" prefWidth="200.0" BorderPane.alignment="CENTER" />
</left>
</BorderPane>