package nl.andrewlalis.crystalkeep.io.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.ShardType; import nl.andrewlalis.crystalkeep.model.shards.TextShard; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import static nl.andrewlalis.crystalkeep.io.serialization.ByteUtils.toInt; /** * This serializer class offers some methods for reading and writing clusters * from and to byte arrays. */ 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. * @param os The output stream to write to. * @throws IOException If an error occurs while writing the cluster. */ public static void writeCluster(Cluster cluster, OutputStream os) throws IOException { ByteUtils.writeLengthPrefixed(cluster.getName(), os); os.write(ByteUtils.toBytes(cluster.getClusters().size())); for (Cluster child : cluster.getClustersOrdered()) { writeCluster(child, os); } os.write(ByteUtils.toBytes(cluster.getShards().size())); for (Shard shard : cluster.getShardsOrdered()) { writeShard(shard, os); } } /** * Reads a cluster from an input stream. * @param is The input stream to read from. * @return The cluster that was read. * @throws IOException If data could not be read from the stream. */ public static Cluster readCluster(InputStream is) throws IOException { String name = ByteUtils.readLengthPrefixedString(is); Cluster cluster = new Cluster(name, new HashSet<>(), new HashSet<>()); int childCount = toInt(is.readNBytes(4)); for (int i = 0; i < childCount; i++) { cluster.addCluster(readCluster(is)); } int shardCount = toInt(is.readNBytes(4)); for (int i = 0; i < shardCount; i++) { cluster.addShard(readShard(is)); } 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. * @param os The output stream to write to. * @throws IOException If byte array stream could not be written to. */ @SuppressWarnings({"unchecked", "rawtypes"}) public static void writeShard(Shard shard, OutputStream os) throws IOException { ByteUtils.writeLengthPrefixed(shard.getName().getBytes(StandardCharsets.UTF_8), os); ByteUtils.writeLengthPrefixed(shard.getCreatedAt().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME).getBytes(StandardCharsets.UTF_8), os); os.write(ByteUtils.toBytes(shard.getType().getValue())); ShardSerializer serializer = serializers.get(shard.getType()); os.write(serializer.serialize(shard)); } /** * Deserializes a shard from a byte array that's being read by the given * input stream. * @param is The input stream to read from. * @return The shard that was deserialized. * @throws IOException If an error occurs while reading bytes. */ public static Shard readShard(InputStream is) 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, name, createdAt); } }