Skip to content

Commit

Permalink
Token serializer
Browse files Browse the repository at this point in the history
Signed-off-by: Thomas Gran <[email protected]>
  • Loading branch information
t2gran committed Nov 16, 2023
1 parent 583e847 commit 3523861
Show file tree
Hide file tree
Showing 14 changed files with 997 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,17 @@ public static <E, T> T ifNotNull(
}

public static <T> T requireNotInitialized(T oldValue, T newValue) {
return requireNotInitialized(null, oldValue, newValue);
}

public static <T> T requireNotInitialized(@Nullable String name, T oldValue, T newValue) {
if (oldValue != null) {
throw new IllegalStateException(
"Field is already set! Old value: " + oldValue + ", new value: " + newValue
"Field%s is already set! Old value: %s, new value: %s.".formatted(
(name == null ? "" : " " + name),
oldValue,
newValue
)
);
}
return newValue;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package org.opentripplanner.framework.token;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import javax.annotation.Nullable;
import org.opentripplanner.framework.time.DurationUtils;

class Deserializer {

private final ByteArrayInputStream input;

Deserializer(String token) {
this.input = new ByteArrayInputStream(Base64.getUrlDecoder().decode(token));
}

List<Object> deserialize(TokenDefinition definition) throws IOException {
try {
// Assume deprecated fields are included in the token
return readFields(definition, false);
} catch (IOException ignore) {
// If the token is the next version, then deprecated field are removed. Try
// skipping the deprecated tokens
return readFields(definition, true);
}
}

private List<Object> readFields(TokenDefinition definition, boolean matchNewVersionPlusOne)
throws IOException {
input.reset();
List<Object> result = new ArrayList<>();

var in = new ObjectInputStream(input);

readAndMatchVersion(in, definition, matchNewVersionPlusOne);

for (FieldDefinition field : definition.listFields()) {
if (matchNewVersionPlusOne && field.deprecated()) {
continue;
}
var v = read(in, field);
if (!field.deprecated()) {
result.add(v);
}
}
return result;
}

private void readAndMatchVersion(
ObjectInputStream in,
TokenDefinition definition,
boolean matchVersionPlusOne
) throws IOException {
int matchVersion = (matchVersionPlusOne ? 1 : 0) + definition.version();

int v = readInt(in);
if (v != matchVersion) {
throw new IOException(
"Version does not match. Token version: " + v + ", schema version: " + definition.version()
);
}
}

private Object read(ObjectInputStream in, FieldDefinition field) throws IOException {
return switch (field.type()) {
case BYTE -> readByte(in);
case DURATION -> readDuration(in);
case INT -> readInt(in);
case STRING -> readString(in);
case TIME_INSTANT -> readTimeInstant(in);
};
}

private static byte readByte(ObjectInputStream in) throws IOException {
return in.readByte();
}

private static int readInt(ObjectInputStream in) throws IOException {
return Integer.parseInt(in.readUTF());
}

private static String readString(ObjectInputStream in) throws IOException {
return in.readUTF();
}

private static Duration readDuration(ObjectInputStream in) throws IOException {
return DurationUtils.duration(in.readUTF());
}

@Nullable
private static Instant readTimeInstant(ObjectInputStream in) throws IOException {
return Instant.parse(in.readUTF());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package org.opentripplanner.framework.token;

import java.util.Objects;

class FieldDefinition {

private final String name;
private final TokenType type;
private final boolean deprecated;

private FieldDefinition(String name, TokenType type, boolean deprecated) {
this.name = Objects.requireNonNull(name);
this.type = Objects.requireNonNull(type);
this.deprecated = deprecated;
}

public FieldDefinition(String name, TokenType type) {
this(name, type, false);
}

public String name() {
return name;
}

public TokenType type() {
return type;
}

public boolean deprecated() {
return deprecated;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
FieldDefinition that = (FieldDefinition) o;
return deprecated == that.deprecated && Objects.equals(name, that.name) && type == that.type;
}

@Override
public int hashCode() {
return Objects.hash(name, type, deprecated);
}

@Override
public String toString() {
return (deprecated ? "@deprecated " : "") + name + ":" + type;
}

public FieldDefinition deprecate() {
return new FieldDefinition(name, type, true);
}
}
64 changes: 64 additions & 0 deletions src/main/java/org/opentripplanner/framework/token/Serializer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package org.opentripplanner.framework.token;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.time.Duration;
import java.time.Instant;
import java.util.Base64;
import org.opentripplanner.framework.time.DurationUtils;

class Serializer {

private final TokenDefinition definition;
private final ByteArrayOutputStream buf = new ByteArrayOutputStream();

Serializer(TokenDefinition definition) {
this.definition = definition;
}

String serialize(Object[] values) throws IOException {
try (var out = new ObjectOutputStream(buf)) {
writeInt(out, definition.version());

for (var fieldName : definition.fieldNames()) {
var value = values[definition.index(fieldName)];
write(out, fieldName, value);
}
out.flush();
}
return Base64.getUrlEncoder().encodeToString(buf.toByteArray());
}

private void write(ObjectOutputStream out, String fieldName, Object value) throws IOException {
var type = definition.type(fieldName);
switch (type) {
case BYTE -> writeByte(out, (byte) value);
case DURATION -> writeDuration(out, (Duration) value);
case INT -> writeInt(out, (int) value);
case STRING -> writeString(out, (String) value);
case TIME_INSTANT -> writeTimeInstant(out, (Instant) value);
default -> throw new IllegalArgumentException("Unknown type: " + type);
}
}

private static void writeByte(ObjectOutputStream out, byte value) throws IOException {
out.writeByte(value);
}

private static void writeInt(ObjectOutputStream out, int value) throws IOException {
out.writeUTF(Integer.toString(value));
}

private static void writeString(ObjectOutputStream out, String value) throws IOException {
out.writeUTF(value);
}

private static void writeDuration(ObjectOutputStream out, Duration duration) throws IOException {
out.writeUTF(DurationUtils.durationToStr(duration));
}

private static void writeTimeInstant(ObjectOutputStream out, Instant time) throws IOException {
out.writeUTF(time.toString());
}
}
61 changes: 61 additions & 0 deletions src/main/java/org/opentripplanner/framework/token/Token.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package org.opentripplanner.framework.token;

import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

/**
* Given a schema definition and a token version this class holds the values for
* all fields in a token.
*/
public class Token {

private final TokenDefinition definition;
private final List<Object> fieldValues;

Token(TokenDefinition definition, List<Object> fieldValues) {
this.definition = Objects.requireNonNull(definition);
this.fieldValues = Objects.requireNonNull(fieldValues);
}

public int version() {
return definition.version();
}

public byte getByte(String fieldName) {
return (byte) get(fieldName, TokenType.BYTE);
}

public Duration getDuration(String fieldName) {
return (Duration) get(fieldName, TokenType.DURATION);
}

public int getInt(String fieldName) {
return (int) get(fieldName, TokenType.INT);
}

public String getString(String fieldName) {
return (String) get(fieldName, TokenType.STRING);
}

public Instant getTimeInstant(String fieldName) {
return (Instant) get(fieldName, TokenType.TIME_INSTANT);
}

private Object get(String fieldName, TokenType type) {
return fieldValues.get(definition.getIndex(fieldName, type));
}

@Override
public String toString() {
return (
"(v" +
version() +
", " +
fieldValues.stream().map(Objects::toString).collect(Collectors.joining(", ")) +
')'
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package org.opentripplanner.framework.token;

import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import org.opentripplanner.framework.lang.ObjectUtils;

/**
* This class is used to create a {@link Token} before encoding it.
*/
public class TokenBuilder {

private final TokenDefinition definition;
private final Object[] values;

public TokenBuilder(TokenDefinition definition) {
this.definition = definition;
this.values = new Object[definition.size()];
}

public TokenBuilder withByte(String fieldName, byte v) {
return with(fieldName, TokenType.BYTE, v);
}

public TokenBuilder withDuration(String fieldName, Duration v) {
return with(fieldName, TokenType.DURATION, v);
}

public TokenBuilder withInt(String fieldName, int v) {
return with(fieldName, TokenType.INT, v);
}

public TokenBuilder withString(String fieldName, String v) {
return with(fieldName, TokenType.STRING, v);
}

public TokenBuilder withTimeInstant(String fieldName, Instant v) {
return with(fieldName, TokenType.TIME_INSTANT, v);
}

public String build() {
try {
return new Serializer(definition).serialize(values);
} catch (IOException e) {
throw new RuntimeException(e.getMessage(), e);
}
}

private TokenBuilder with(String fieldName, TokenType type, Object value) {
int index = definition.getIndex(fieldName, type);
ObjectUtils.requireNotInitialized(fieldName, values[index], value);
values[index] = value;
return this;
}
}
Loading

0 comments on commit 3523861

Please sign in to comment.