1067 lines
38 KiB
Java
1067 lines
38 KiB
Java
package com.hypixel.hytale.codec.builder;
|
|
|
|
import com.hypixel.hytale.codec.Codec;
|
|
import com.hypixel.hytale.codec.DirectDecodeCodec;
|
|
import com.hypixel.hytale.codec.EmptyExtraInfo;
|
|
import com.hypixel.hytale.codec.ExtraInfo;
|
|
import com.hypixel.hytale.codec.InheritCodec;
|
|
import com.hypixel.hytale.codec.KeyedCodec;
|
|
import com.hypixel.hytale.codec.RawJsonCodec;
|
|
import com.hypixel.hytale.codec.VersionedExtraInfo;
|
|
import com.hypixel.hytale.codec.exception.CodecException;
|
|
import com.hypixel.hytale.codec.schema.SchemaContext;
|
|
import com.hypixel.hytale.codec.schema.config.NullSchema;
|
|
import com.hypixel.hytale.codec.schema.config.ObjectSchema;
|
|
import com.hypixel.hytale.codec.schema.config.Schema;
|
|
import com.hypixel.hytale.codec.schema.metadata.Metadata;
|
|
import com.hypixel.hytale.codec.schema.metadata.ui.UIDisplayMode;
|
|
import com.hypixel.hytale.codec.util.RawJsonReader;
|
|
import com.hypixel.hytale.codec.validation.ValidatableCodec;
|
|
import com.hypixel.hytale.codec.validation.ValidationResults;
|
|
import com.hypixel.hytale.function.consumer.TriConsumer;
|
|
import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap;
|
|
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
|
import java.io.IOException;
|
|
import java.util.Collections;
|
|
import java.util.Comparator;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Objects;
|
|
import java.util.Set;
|
|
import java.util.Map.Entry;
|
|
import java.util.function.BiConsumer;
|
|
import java.util.function.BiFunction;
|
|
import java.util.function.Consumer;
|
|
import java.util.function.Function;
|
|
import java.util.function.Supplier;
|
|
import javax.annotation.Nonnull;
|
|
import javax.annotation.Nullable;
|
|
import org.bson.BsonDocument;
|
|
import org.bson.BsonValue;
|
|
|
|
public class BuilderCodec<T> implements Codec<T>, DirectDecodeCodec<T>, RawJsonCodec<T>, InheritCodec<T>, ValidatableCodec<T> {
|
|
public static final int UNSET_VERSION = Integer.MIN_VALUE;
|
|
public static final int UNSET_MAX_VERSION = Integer.MAX_VALUE;
|
|
public static final int INITIAL_VERSION = 0;
|
|
public static final BuilderCodec<?>[] EMPTY_ARRAY = new BuilderCodec[0];
|
|
private static final KeyedCodec<Integer> VERSION = new KeyedCodec<>("Version", INTEGER);
|
|
protected final Class<T> tClass;
|
|
protected final Supplier<T> supplier;
|
|
@Nullable
|
|
protected final BuilderCodec<? super T> parentCodec;
|
|
@Nonnull
|
|
protected final Map<String, List<BuilderField<T, ?>>> entries;
|
|
@Nonnull
|
|
protected final Map<String, List<BuilderField<T, ?>>> unmodifiableEntries;
|
|
protected final BiConsumer<T, ValidationResults> validator;
|
|
protected final BiConsumer<T, ExtraInfo> afterDecode;
|
|
protected final boolean hasNonNullValidator;
|
|
protected final String documentation;
|
|
protected final List<Metadata> metadata;
|
|
protected final int codecVersion;
|
|
protected final int minCodecVersion;
|
|
protected final boolean versioned;
|
|
/** @deprecated */
|
|
protected final boolean useLegacyVersion;
|
|
@Nonnull
|
|
protected final StringTreeMap<BuilderCodec.KeyEntry<T>> stringTreeMap;
|
|
|
|
protected BuilderCodec(@Nonnull BuilderCodec.BuilderBase<T, ?> builder) {
|
|
this.tClass = builder.tClass;
|
|
this.supplier = builder.supplier;
|
|
this.parentCodec = builder.parentCodec;
|
|
this.entries = Objects.requireNonNull(builder.entries, "entries parameter can't be null");
|
|
this.unmodifiableEntries = Collections.unmodifiableMap(builder.entries);
|
|
this.stringTreeMap = builder.stringTreeMap;
|
|
this.validator = builder.validator;
|
|
this.afterDecode = builder.afterDecode;
|
|
this.documentation = builder.documentation;
|
|
this.metadata = builder.metadata;
|
|
boolean hasNonNullValidator = false;
|
|
|
|
for (List<BuilderField<T, ?>> fields : this.entries.values()) {
|
|
fields.sort(Comparator.comparingInt(BuilderField::getMinVersion));
|
|
|
|
for (BuilderField<T, ?> field : fields) {
|
|
hasNonNullValidator |= field.hasNonNullValidator();
|
|
}
|
|
}
|
|
|
|
this.hasNonNullValidator = hasNonNullValidator;
|
|
int codecVersion;
|
|
if (builder.codecVersion != Integer.MIN_VALUE) {
|
|
codecVersion = builder.codecVersion;
|
|
} else {
|
|
int highestFieldVersion = Integer.MIN_VALUE;
|
|
|
|
for (List<BuilderField<T, ?>> fields : this.entries.values()) {
|
|
for (BuilderField<T, ?> field : fields) {
|
|
highestFieldVersion = Math.max(highestFieldVersion, field.getHighestSupportedVersion());
|
|
}
|
|
}
|
|
|
|
codecVersion = highestFieldVersion;
|
|
}
|
|
|
|
int minCodecVersion;
|
|
if (builder.minCodecVersion != Integer.MAX_VALUE) {
|
|
minCodecVersion = builder.minCodecVersion;
|
|
} else {
|
|
int lowestFieldVersion = Integer.MAX_VALUE;
|
|
|
|
for (List<BuilderField<T, ?>> fields : this.entries.values()) {
|
|
for (BuilderField<T, ?> field : fields) {
|
|
int min = field.getMinVersion();
|
|
if (min != Integer.MIN_VALUE) {
|
|
lowestFieldVersion = Math.min(lowestFieldVersion, min);
|
|
}
|
|
}
|
|
}
|
|
|
|
minCodecVersion = lowestFieldVersion;
|
|
}
|
|
|
|
if (this.parentCodec != null) {
|
|
codecVersion = Math.max(codecVersion, this.parentCodec.codecVersion);
|
|
minCodecVersion = Math.min(minCodecVersion, this.parentCodec.minCodecVersion);
|
|
this.versioned = builder.versioned || this.parentCodec.versioned;
|
|
this.useLegacyVersion = builder.useLegacyVersion || this.parentCodec.useLegacyVersion;
|
|
} else {
|
|
this.versioned = builder.versioned;
|
|
this.useLegacyVersion = builder.useLegacyVersion;
|
|
}
|
|
|
|
this.codecVersion = codecVersion;
|
|
this.minCodecVersion = minCodecVersion;
|
|
}
|
|
|
|
public Class<T> getInnerClass() {
|
|
return this.tClass;
|
|
}
|
|
|
|
public Supplier<T> getSupplier() {
|
|
return this.supplier;
|
|
}
|
|
|
|
public T getDefaultValue() {
|
|
return this.getDefaultValue(ExtraInfo.THREAD_LOCAL.get());
|
|
}
|
|
|
|
public T getDefaultValue(ExtraInfo extraInfo) {
|
|
T t = this.supplier.get();
|
|
this.afterDecode(t, extraInfo);
|
|
return t;
|
|
}
|
|
|
|
@Nonnull
|
|
public Map<String, List<BuilderField<T, ?>>> getEntries() {
|
|
return this.unmodifiableEntries;
|
|
}
|
|
|
|
public BiConsumer<T, ExtraInfo> getAfterDecode() {
|
|
return this.afterDecode;
|
|
}
|
|
|
|
@Nullable
|
|
public BuilderCodec<? super T> getParent() {
|
|
return this.parentCodec;
|
|
}
|
|
|
|
public String getDocumentation() {
|
|
return this.documentation;
|
|
}
|
|
|
|
public int getCodecVersion() {
|
|
return this.codecVersion;
|
|
}
|
|
|
|
public void inherit(T t, @Nonnull T parent, @Nonnull ExtraInfo extraInfo) {
|
|
if (this.parentCodec != null) {
|
|
this.parentCodec.inherit(t, parent, extraInfo);
|
|
}
|
|
|
|
if (this.getInnerClass().isAssignableFrom(parent.getClass())) {
|
|
for (List<BuilderField<T, ?>> entry : this.entries.values()) {
|
|
BuilderField<T, ?> field = findField(entry, extraInfo);
|
|
if (field != null) {
|
|
field.inherit(t, parent, extraInfo);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public void afterDecode(T t, ExtraInfo extraInfo) {
|
|
if (this.parentCodec != null) {
|
|
this.parentCodec.afterDecode(t, extraInfo);
|
|
}
|
|
|
|
if (this.afterDecode != null) {
|
|
this.afterDecode.accept(t, extraInfo);
|
|
}
|
|
}
|
|
|
|
public void afterDecodeAndValidate(T t, @Nonnull ExtraInfo extraInfo) {
|
|
if (this.parentCodec != null) {
|
|
this.parentCodec.afterDecodeAndValidate(t, extraInfo);
|
|
}
|
|
|
|
if (this.afterDecode != null) {
|
|
this.afterDecode.accept(t, extraInfo);
|
|
}
|
|
|
|
ValidationResults results = extraInfo.getValidationResults();
|
|
if (this.hasNonNullValidator) {
|
|
for (List<BuilderField<T, ?>> entry : this.entries.values()) {
|
|
BuilderField<T, ?> field = findField(entry, extraInfo);
|
|
if (field != null) {
|
|
extraInfo.pushKey(field.codec.getKey());
|
|
|
|
try {
|
|
field.nullValidate(t, results, extraInfo);
|
|
} finally {
|
|
extraInfo.popKey();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this.validator != null) {
|
|
this.validator.accept(t, results);
|
|
}
|
|
|
|
results._processValidationResults();
|
|
} else if (this.validator != null) {
|
|
this.validator.accept(t, results);
|
|
results._processValidationResults();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public T decode(@Nonnull BsonValue bsonValue, @Nonnull ExtraInfo extraInfo) {
|
|
if (this.supplier == null) {
|
|
throw new CodecException("This BuilderCodec is for an abstract or direct codec. To use this codec you must specify an existing object to decode into.");
|
|
} else {
|
|
T t = this.supplier.get();
|
|
this.decode(bsonValue.asDocument(), t, extraInfo);
|
|
return t;
|
|
}
|
|
}
|
|
|
|
@Nonnull
|
|
public BsonDocument encode(T t, @Nonnull ExtraInfo extraInfo) {
|
|
BsonDocument document = new BsonDocument();
|
|
if (this.versioned) {
|
|
if (this.codecVersion != 0) {
|
|
VERSION.put(document, this.codecVersion, extraInfo);
|
|
}
|
|
|
|
extraInfo = new VersionedExtraInfo(this.codecVersion, extraInfo);
|
|
}
|
|
|
|
return this.encode0(t, document, extraInfo);
|
|
}
|
|
|
|
@Override
|
|
public void decode(@Nonnull BsonValue bsonValue, T t, @Nonnull ExtraInfo extraInfo) {
|
|
this.decode0(bsonValue.asDocument(), t, extraInfo);
|
|
this.afterDecodeAndValidate(t, extraInfo);
|
|
}
|
|
|
|
protected void decode0(@Nonnull BsonDocument document, T t, ExtraInfo extraInfo) {
|
|
if (this.versioned) {
|
|
extraInfo = this.decodeVersion(document, extraInfo);
|
|
}
|
|
|
|
for (Entry<String, BsonValue> entry : document.entrySet()) {
|
|
String key = entry.getKey();
|
|
BuilderField<? super T, ?> field = findEntry(this, key, extraInfo);
|
|
if (field != null) {
|
|
field.decode(document, t, extraInfo);
|
|
} else {
|
|
extraInfo.addUnknownKey(key);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Nonnull
|
|
protected BsonDocument encode0(T t, @Nonnull BsonDocument document, @Nonnull ExtraInfo extraInfo) {
|
|
if (this.parentCodec != null) {
|
|
this.parentCodec.encode0(t, document, extraInfo);
|
|
}
|
|
|
|
for (List<BuilderField<T, ?>> entry : this.entries.values()) {
|
|
BuilderField<T, ?> field = findField(entry, extraInfo);
|
|
if (field != null) {
|
|
field.encode(document, t, extraInfo);
|
|
}
|
|
}
|
|
|
|
return document;
|
|
}
|
|
|
|
@Override
|
|
public T decodeJson(@Nonnull RawJsonReader reader, @Nonnull ExtraInfo extraInfo) throws IOException {
|
|
if (this.supplier == null) {
|
|
throw new CodecException("This BuilderCodec is for an abstract or direct codec. To use this codec you must specify an existing object to decode into.");
|
|
} else {
|
|
T t = this.supplier.get();
|
|
this.decodeJson0(reader, t, extraInfo);
|
|
this.afterDecodeAndValidate(t, extraInfo);
|
|
return t;
|
|
}
|
|
}
|
|
|
|
public void decodeJson0(@Nonnull RawJsonReader reader, T t, ExtraInfo extraInfo) throws IOException {
|
|
if (this.versioned) {
|
|
extraInfo = this.decodeVersion(reader, extraInfo);
|
|
}
|
|
|
|
reader.expect('{');
|
|
reader.consumeWhiteSpace();
|
|
if (!reader.tryConsume('}')) {
|
|
while (true) {
|
|
this.readEntry(reader, t, extraInfo);
|
|
reader.consumeWhiteSpace();
|
|
if (reader.tryConsumeOrExpect('}', ',')) {
|
|
return;
|
|
}
|
|
|
|
reader.consumeWhiteSpace();
|
|
}
|
|
}
|
|
}
|
|
|
|
protected void readEntry(@Nonnull RawJsonReader reader, T t, @Nonnull ExtraInfo extraInfo) throws IOException {
|
|
reader.mark();
|
|
StringTreeMap<BuilderCodec.KeyEntry<T>> treeMapEntry = this.stringTreeMap.findEntry(reader);
|
|
BuilderCodec.KeyEntry<T> keyEntry;
|
|
if (treeMapEntry != null && (keyEntry = treeMapEntry.getValue()) != null) {
|
|
switch (keyEntry.getType()) {
|
|
case FIELD:
|
|
String key = treeMapEntry.getKey();
|
|
List<BuilderField<T, ?>> fields = keyEntry.getFields();
|
|
this.readField(reader, t, extraInfo, key, fields);
|
|
break;
|
|
case IGNORE:
|
|
reader.unmark();
|
|
this.skipField(reader);
|
|
break;
|
|
case IGNORE_IN_BASE_OBJECT:
|
|
if (extraInfo.getKeysSize() == 0) {
|
|
reader.unmark();
|
|
this.skipField(reader);
|
|
} else {
|
|
reader.reset();
|
|
this.readUnknownField(reader, extraInfo);
|
|
}
|
|
break;
|
|
default:
|
|
throw new IllegalArgumentException("Unknown field entry type: " + keyEntry.getType());
|
|
}
|
|
} else {
|
|
reader.reset();
|
|
this.readUnknownField(reader, extraInfo);
|
|
}
|
|
}
|
|
|
|
private void skipField(@Nonnull RawJsonReader reader) throws IOException {
|
|
reader.consumeWhiteSpace();
|
|
reader.expect(':');
|
|
reader.consumeWhiteSpace();
|
|
reader.skipValue();
|
|
}
|
|
|
|
private void readField(@Nonnull RawJsonReader reader, T t, @Nonnull ExtraInfo extraInfo, String key, @Nonnull List<BuilderField<T, ?>> fields) throws IOException {
|
|
BuilderField<T, ?> entry = null;
|
|
|
|
for (BuilderField<T, ?> field : fields) {
|
|
if (field.supportsVersion(extraInfo.getVersion())) {
|
|
entry = field;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (entry == null) {
|
|
reader.reset();
|
|
this.readUnknownField(reader, extraInfo);
|
|
} else {
|
|
reader.unmark();
|
|
reader.consumeWhiteSpace();
|
|
reader.expect(':');
|
|
reader.consumeWhiteSpace();
|
|
extraInfo.pushKey(key, reader);
|
|
|
|
try {
|
|
entry.decodeJson(reader, t, extraInfo);
|
|
} catch (Exception var12) {
|
|
throw new CodecException("Failed to decode", reader, extraInfo, var12);
|
|
} finally {
|
|
extraInfo.popKey();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void readUnknownField(@Nonnull RawJsonReader reader, @Nonnull ExtraInfo extraInfo) throws IOException {
|
|
extraInfo.readUnknownKey(reader);
|
|
reader.consumeWhiteSpace();
|
|
reader.expect(':');
|
|
reader.consumeWhiteSpace();
|
|
reader.skipValue();
|
|
}
|
|
|
|
public void decodeJson(@Nonnull RawJsonReader reader, T t, @Nonnull ExtraInfo extraInfo) throws IOException {
|
|
this.decodeJson0(reader, t, extraInfo);
|
|
this.afterDecodeAndValidate(t, extraInfo);
|
|
}
|
|
|
|
@Override
|
|
public T decodeAndInherit(@Nonnull BsonDocument document, T parent, ExtraInfo extraInfo) {
|
|
T t = this.supplier.get();
|
|
this.decodeAndInherit(document, t, parent, extraInfo);
|
|
return t;
|
|
}
|
|
|
|
@Override
|
|
public void decodeAndInherit(@Nonnull BsonDocument document, T t, @Nullable T parent, ExtraInfo extraInfo) {
|
|
if (this.versioned) {
|
|
extraInfo = this.decodeVersion(document, extraInfo);
|
|
}
|
|
|
|
if (parent != null) {
|
|
this.inherit(t, parent, extraInfo);
|
|
}
|
|
|
|
this.decodeAndInherit0(document, t, parent, extraInfo);
|
|
this.afterDecodeAndValidate(t, extraInfo);
|
|
}
|
|
|
|
protected void decodeAndInherit0(@Nonnull BsonDocument document, T t, T parent, @Nonnull ExtraInfo extraInfo) {
|
|
for (Entry<String, BsonValue> entry : document.entrySet()) {
|
|
String key = entry.getKey();
|
|
BuilderField<? super T, ?> field = findEntry(this, key, extraInfo);
|
|
if (field != null) {
|
|
if (field.codec.getChildCodec() instanceof BuilderCodec) {
|
|
decodeAndInherit(field, document, t, parent, extraInfo);
|
|
} else {
|
|
field.decodeAndInherit(document, t, parent, extraInfo);
|
|
}
|
|
} else {
|
|
extraInfo.addUnknownKey(key);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static <Type, FieldType> void decodeAndInherit(
|
|
@Nonnull BuilderField<Type, FieldType> entry, @Nonnull BsonDocument document, Type t, @Nullable Type parent, @Nonnull ExtraInfo extraInfo
|
|
) {
|
|
KeyedCodec<FieldType> codec = entry.codec;
|
|
String key = codec.getKey();
|
|
BsonValue bsonValue = document.get(key);
|
|
if (Codec.isNullBsonValue(bsonValue)) {
|
|
if (bsonValue != null && bsonValue.isNull()) {
|
|
entry.setValue(t, null, extraInfo);
|
|
}
|
|
} else {
|
|
extraInfo.pushKey(key);
|
|
|
|
try {
|
|
BuilderCodec<FieldType> inheritCodec = (BuilderCodec<FieldType>)codec.getChildCodec();
|
|
FieldType value = inheritCodec.getSupplier().get();
|
|
FieldType parentValue = parent != null ? entry.getter.apply(parent, extraInfo) : null;
|
|
inheritCodec.decodeAndInherit(bsonValue.asDocument(), value, parentValue, extraInfo);
|
|
entry.setValue(t, value, extraInfo);
|
|
} catch (Exception var14) {
|
|
throw new CodecException("Failed to decode", bsonValue, extraInfo, var14);
|
|
} finally {
|
|
extraInfo.popKey();
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public T decodeAndInheritJson(@Nonnull RawJsonReader reader, T parent, ExtraInfo extraInfo) throws IOException {
|
|
T t = this.supplier.get();
|
|
this.decodeAndInheritJson(reader, t, parent, extraInfo);
|
|
return t;
|
|
}
|
|
|
|
@Override
|
|
public void decodeAndInheritJson(@Nonnull RawJsonReader reader, T t, @Nullable T parent, ExtraInfo extraInfo) throws IOException {
|
|
if (this.versioned) {
|
|
extraInfo = this.decodeVersion(reader, extraInfo);
|
|
}
|
|
|
|
if (parent != null) {
|
|
this.inherit(t, parent, extraInfo);
|
|
}
|
|
|
|
this.decodeAndInheritJson0(reader, t, parent, extraInfo);
|
|
this.afterDecodeAndValidate(t, extraInfo);
|
|
}
|
|
|
|
public void decodeAndInheritJson0(@Nonnull RawJsonReader reader, T t, T parent, @Nonnull ExtraInfo extraInfo) throws IOException {
|
|
reader.expect('{');
|
|
reader.consumeWhiteSpace();
|
|
if (!reader.tryConsume('}')) {
|
|
while (true) {
|
|
this.readAndInheritEntry(reader, t, parent, extraInfo);
|
|
reader.consumeWhiteSpace();
|
|
if (reader.tryConsumeOrExpect('}', ',')) {
|
|
return;
|
|
}
|
|
|
|
reader.consumeWhiteSpace();
|
|
}
|
|
}
|
|
}
|
|
|
|
protected void readAndInheritEntry(@Nonnull RawJsonReader reader, T t, T parent, @Nonnull ExtraInfo extraInfo) throws IOException {
|
|
reader.mark();
|
|
StringTreeMap<BuilderCodec.KeyEntry<T>> treeMapEntry = this.stringTreeMap.findEntry(reader);
|
|
BuilderCodec.KeyEntry<T> keyEntry;
|
|
if (treeMapEntry != null && (keyEntry = treeMapEntry.getValue()) != null) {
|
|
switch (keyEntry.getType()) {
|
|
case FIELD:
|
|
String key = treeMapEntry.getKey();
|
|
List<BuilderField<T, ?>> fields = keyEntry.getFields();
|
|
this.readAndInheritField(reader, t, parent, extraInfo, key, fields);
|
|
break;
|
|
case IGNORE:
|
|
reader.unmark();
|
|
this.skipField(reader);
|
|
break;
|
|
case IGNORE_IN_BASE_OBJECT:
|
|
if (extraInfo.getKeysSize() == 0) {
|
|
reader.unmark();
|
|
this.skipField(reader);
|
|
} else {
|
|
reader.reset();
|
|
this.readUnknownField(reader, extraInfo);
|
|
}
|
|
}
|
|
} else {
|
|
reader.reset();
|
|
this.readUnknownField(reader, extraInfo);
|
|
}
|
|
}
|
|
|
|
private void readAndInheritField(
|
|
@Nonnull RawJsonReader reader, T t, T parent, @Nonnull ExtraInfo extraInfo, String key, @Nonnull List<BuilderField<T, ?>> fields
|
|
) throws IOException {
|
|
BuilderField<T, ?> entry = null;
|
|
|
|
for (BuilderField<T, ?> field : fields) {
|
|
if (field.supportsVersion(extraInfo.getVersion())) {
|
|
entry = field;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (entry == null) {
|
|
reader.reset();
|
|
this.readUnknownField(reader, extraInfo);
|
|
} else {
|
|
reader.unmark();
|
|
reader.consumeWhiteSpace();
|
|
reader.expect(':');
|
|
reader.consumeWhiteSpace();
|
|
extraInfo.pushKey(key, reader);
|
|
|
|
try {
|
|
if (entry.codec.getChildCodec() instanceof BuilderCodec) {
|
|
decodeAndInheritJson(entry, reader, t, parent, extraInfo);
|
|
} else {
|
|
entry.decodeAndInheritJson(reader, t, parent, extraInfo);
|
|
}
|
|
} catch (Exception var13) {
|
|
throw new CodecException("Failed to decode", reader, extraInfo, var13);
|
|
} finally {
|
|
extraInfo.popKey();
|
|
}
|
|
}
|
|
}
|
|
|
|
private static <Type, FieldType> void decodeAndInheritJson(
|
|
@Nonnull BuilderField<Type, FieldType> entry, @Nonnull RawJsonReader reader, Type t, @Nullable Type parent, @Nonnull ExtraInfo extraInfo
|
|
) throws IOException {
|
|
int read = reader.peek();
|
|
if (read == -1) {
|
|
throw new IOException("Unexpected EOF!");
|
|
} else {
|
|
switch (read) {
|
|
case 78:
|
|
case 110:
|
|
reader.readNullValue();
|
|
entry.setValue(t, null, extraInfo);
|
|
return;
|
|
default:
|
|
BuilderCodec<FieldType> inheritCodec = (BuilderCodec<FieldType>)entry.codec.getChildCodec();
|
|
FieldType value = inheritCodec.getSupplier().get();
|
|
FieldType parentValue = parent != null ? entry.getter.apply(parent, extraInfo) : null;
|
|
inheritCodec.decodeAndInheritJson(reader, value, parentValue, extraInfo);
|
|
entry.setValue(t, value, extraInfo);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Nonnull
|
|
protected ExtraInfo decodeVersion(BsonDocument document, @Nonnull ExtraInfo extraInfo) {
|
|
if (this.useLegacyVersion && extraInfo.getLegacyVersion() != Integer.MAX_VALUE) {
|
|
return new VersionedExtraInfo(extraInfo.getLegacyVersion(), extraInfo);
|
|
} else {
|
|
int version = VERSION.get(document, extraInfo).orElse(0);
|
|
if (version > this.codecVersion) {
|
|
throw new IllegalArgumentException("Version " + version + " is newer than expected version " + this.codecVersion);
|
|
} else if (this.minCodecVersion != Integer.MAX_VALUE && version < this.minCodecVersion) {
|
|
throw new IllegalArgumentException("Version " + version + " is older than min supported version " + this.minCodecVersion);
|
|
} else {
|
|
return new VersionedExtraInfo(version, extraInfo);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Nonnull
|
|
protected ExtraInfo decodeVersion(@Nonnull RawJsonReader reader, @Nonnull ExtraInfo extraInfo) throws IOException {
|
|
if (this.useLegacyVersion && extraInfo.getLegacyVersion() != Integer.MAX_VALUE) {
|
|
return new VersionedExtraInfo(extraInfo.getLegacyVersion(), extraInfo);
|
|
} else {
|
|
reader.mark();
|
|
int version = 0;
|
|
if (RawJsonReader.seekToKey(reader, VERSION.getKey())) {
|
|
version = reader.readIntValue();
|
|
}
|
|
|
|
if (version > this.codecVersion) {
|
|
throw new IllegalArgumentException("Version " + version + " is newer than expected version " + this.codecVersion);
|
|
} else if (this.minCodecVersion != Integer.MAX_VALUE && version < this.minCodecVersion) {
|
|
throw new IllegalArgumentException("Version " + version + " is older than min supported version " + this.minCodecVersion);
|
|
} else {
|
|
reader.reset();
|
|
extraInfo.ignoreUnusedKey(VERSION.getKey());
|
|
return new VersionedExtraInfo(version, extraInfo);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void validate(T t, @Nonnull ExtraInfo extraInfo) {
|
|
if (this.parentCodec != null) {
|
|
this.parentCodec.validate(t, extraInfo);
|
|
}
|
|
|
|
for (List<BuilderField<T, ?>> entry : this.entries.values()) {
|
|
BuilderField<T, ?> field = findField(entry, extraInfo);
|
|
if (field != null) {
|
|
extraInfo.pushKey(field.codec.getKey());
|
|
|
|
try {
|
|
field.validate(t, extraInfo);
|
|
} finally {
|
|
extraInfo.popKey();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void validateDefaults(@Nonnull ExtraInfo extraInfo, @Nonnull Set<Codec<?>> tested) {
|
|
if (tested.add(this)) {
|
|
T t = this.supplier.get();
|
|
this.afterDecode(t, extraInfo);
|
|
|
|
for (BuilderCodec<T> codec = this; codec != null; codec = (BuilderCodec<T>)codec.parentCodec) {
|
|
for (List<BuilderField<T, ?>> entry : codec.entries.values()) {
|
|
BuilderField<T, ?> field = findField(entry, extraInfo);
|
|
if (field != null) {
|
|
extraInfo.pushKey(field.codec.getKey());
|
|
|
|
try {
|
|
field.validateDefaults(t, extraInfo, tested);
|
|
} finally {
|
|
extraInfo.popKey();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Nonnull
|
|
public ObjectSchema toSchema(@Nonnull SchemaContext context) {
|
|
T t = this.getDefaultValue();
|
|
return this.toSchema(context, t);
|
|
}
|
|
|
|
@Nonnull
|
|
public ObjectSchema toSchema(@Nonnull SchemaContext context, @Nullable T def) {
|
|
ObjectSchema schema = new ObjectSchema();
|
|
schema.setAdditionalProperties(false);
|
|
schema.setTitle(this.tClass.getSimpleName());
|
|
schema.setMarkdownDescription(this.documentation);
|
|
schema.getHytale().setMergesProperties(true);
|
|
Map<String, Schema> properties = new Object2ObjectLinkedOpenHashMap();
|
|
if (this.versioned) {
|
|
properties.put(VERSION.getKey(), VERSION.getChildCodec().toSchema(context));
|
|
}
|
|
|
|
Schema comment = new Schema();
|
|
comment.getHytale().setUiPropertyTitle("Comment");
|
|
comment.setDoNotSuggest(true);
|
|
comment.setDescription("Comments don't have any function other than allowing users to add certain internal comments or notes to an asset");
|
|
UIDisplayMode.HIDDEN.modify(comment);
|
|
properties.put("$Title", comment);
|
|
properties.put("$Comment", comment);
|
|
properties.put("$Author", comment);
|
|
properties.put("$TODO", comment);
|
|
properties.put("$Position", comment);
|
|
properties.put("$FloatingFunctionNodes", comment);
|
|
properties.put("$Groups", comment);
|
|
properties.put("$WorkspaceID", comment);
|
|
properties.put("$NodeId", comment);
|
|
properties.put("$NodeEditorMetadata", comment);
|
|
schema.setProperties(properties);
|
|
createSchemaFields(context, def, this, properties);
|
|
if (this.metadata != null) {
|
|
for (int i = 0; i < this.metadata.size(); i++) {
|
|
Metadata meta = this.metadata.get(i);
|
|
meta.modify(schema);
|
|
}
|
|
}
|
|
|
|
return schema;
|
|
}
|
|
|
|
private static <T> void createSchemaFields(
|
|
@Nonnull SchemaContext context, @Nullable T def, @Nonnull BuilderCodec<T> codec, @Nonnull Map<String, Schema> properties
|
|
) {
|
|
if (codec.parentCodec != null) {
|
|
createSchemaFields(context, def, codec.parentCodec, properties);
|
|
}
|
|
|
|
for (Entry<String, List<BuilderField<T, ?>>> entry : codec.getEntries().entrySet()) {
|
|
String key = entry.getKey();
|
|
List<BuilderField<T, ?>> fields = entry.getValue();
|
|
BuilderField<T, ?> field = fields.getLast();
|
|
Codec c = field.getCodec().getChildCodec();
|
|
|
|
Object defC;
|
|
try {
|
|
defC = field.getter.apply(def, EmptyExtraInfo.EMPTY);
|
|
} catch (UnsupportedOperationException var14) {
|
|
continue;
|
|
}
|
|
|
|
Schema fieldSchema = context.refDefinition(c, (T)defC);
|
|
field.updateSchema(context, fieldSchema);
|
|
Schema finalSchema = fieldSchema;
|
|
String type = Schema.CODEC.getIdFor((Class<? extends Schema>)fieldSchema.getClass());
|
|
if (!type.isEmpty()) {
|
|
if (!field.hasNonNullValidator() && !field.isPrimitive) {
|
|
fieldSchema.setTypes(new String[]{type, "null"});
|
|
}
|
|
|
|
properties.put(key, fieldSchema);
|
|
} else if (field.hasNonNullValidator()) {
|
|
properties.put(key, fieldSchema);
|
|
} else {
|
|
properties.put(key, finalSchema = Schema.anyOf(fieldSchema, NullSchema.INSTANCE));
|
|
}
|
|
|
|
finalSchema.setMarkdownDescription(field.getDocumentation());
|
|
}
|
|
}
|
|
|
|
@Nonnull
|
|
@Override
|
|
public String toString() {
|
|
return "BuilderCodec{supplier="
|
|
+ this.supplier
|
|
+ ", parentCodec="
|
|
+ this.parentCodec
|
|
+ ", entries="
|
|
+ this.entries
|
|
+ ", afterDecodeAndValidate="
|
|
+ this.afterDecode
|
|
+ "}";
|
|
}
|
|
|
|
@Nullable
|
|
protected static <T> BuilderField<? super T, ?> findEntry(@Nonnull BuilderCodec<? super T> current, String key, @Nonnull ExtraInfo extraInfo) {
|
|
List<? extends BuilderField<? super T, ?>> fields = current.entries.get(key);
|
|
if (fields != null && fields.size() == 1) {
|
|
BuilderField<? super T, ?> field = (BuilderField<? super T, ?>)fields.getFirst();
|
|
if (field.supportsVersion(extraInfo.getVersion())) {
|
|
return field;
|
|
}
|
|
}
|
|
|
|
BuilderField<? super T, ?> entry;
|
|
for (entry = null; current != null; current = current.parentCodec) {
|
|
entry = findField(current.entries.get(key), extraInfo);
|
|
if (entry != null) {
|
|
return entry;
|
|
}
|
|
}
|
|
|
|
return entry;
|
|
}
|
|
|
|
@Nullable
|
|
protected static <T, F extends BuilderField<T, ?>> F findField(@Nullable List<F> entry, @Nonnull ExtraInfo extraInfo) {
|
|
if (entry == null) {
|
|
return null;
|
|
} else {
|
|
int i = 0;
|
|
|
|
for (int size = entry.size(); i < size; i++) {
|
|
F field = (F)entry.get(i);
|
|
if (field.supportsVersion(extraInfo.getVersion())) {
|
|
return field;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
@Nonnull
|
|
public static <T> BuilderCodec.Builder<T> builder(Class<T> tClass, Supplier<T> supplier) {
|
|
return new BuilderCodec.Builder<>(tClass, supplier);
|
|
}
|
|
|
|
@Nonnull
|
|
public static <T> BuilderCodec.Builder<T> builder(Class<T> tClass, Supplier<T> supplier, BuilderCodec<? super T> parentCodec) {
|
|
return new BuilderCodec.Builder<>(tClass, supplier, parentCodec);
|
|
}
|
|
|
|
@Nonnull
|
|
public static <T> BuilderCodec.Builder<T> abstractBuilder(Class<T> tClass) {
|
|
return new BuilderCodec.Builder<>(tClass, null);
|
|
}
|
|
|
|
@Nonnull
|
|
public static <T> BuilderCodec.Builder<T> abstractBuilder(Class<T> tClass, BuilderCodec<? super T> parentCodec) {
|
|
return new BuilderCodec.Builder<>(tClass, null, parentCodec);
|
|
}
|
|
|
|
public static class Builder<T> extends BuilderCodec.BuilderBase<T, BuilderCodec.Builder<T>> {
|
|
protected Builder(Class<T> tClass, Supplier<T> supplier) {
|
|
super(tClass, supplier);
|
|
}
|
|
|
|
protected Builder(Class<T> tClass, Supplier<T> supplier, @Nullable BuilderCodec<? super T> parentCodec) {
|
|
super(tClass, supplier, parentCodec);
|
|
}
|
|
}
|
|
|
|
public abstract static class BuilderBase<T, S extends BuilderCodec.BuilderBase<T, S>> {
|
|
protected final Class<T> tClass;
|
|
protected final Supplier<T> supplier;
|
|
@Nullable
|
|
protected final BuilderCodec<? super T> parentCodec;
|
|
protected final Map<String, List<BuilderField<T, ?>>> entries = new Object2ObjectLinkedOpenHashMap();
|
|
@Nonnull
|
|
protected final StringTreeMap<BuilderCodec.KeyEntry<T>> stringTreeMap;
|
|
protected BiConsumer<T, ValidationResults> validator;
|
|
protected BiConsumer<T, ExtraInfo> afterDecode;
|
|
protected String documentation;
|
|
protected List<Metadata> metadata;
|
|
protected int codecVersion = Integer.MIN_VALUE;
|
|
protected int minCodecVersion = Integer.MAX_VALUE;
|
|
protected boolean versioned = false;
|
|
protected boolean useLegacyVersion = false;
|
|
|
|
protected BuilderBase(Class<T> tClass, Supplier<T> supplier) {
|
|
this(tClass, supplier, null);
|
|
}
|
|
|
|
protected BuilderBase(Class<T> tClass, Supplier<T> supplier, @Nullable BuilderCodec<? super T> parentCodec) {
|
|
this.tClass = tClass;
|
|
this.supplier = supplier;
|
|
this.parentCodec = parentCodec;
|
|
if (parentCodec != null) {
|
|
this.stringTreeMap = new StringTreeMap<>((StringTreeMap<BuilderCodec.KeyEntry<T>>)parentCodec.stringTreeMap);
|
|
} else {
|
|
this.stringTreeMap = new StringTreeMap<>(
|
|
Map.ofEntries(
|
|
Map.entry("$Title", new BuilderCodec.KeyEntry(BuilderCodec.EntryType.IGNORE)),
|
|
Map.entry("$Comment", new BuilderCodec.KeyEntry(BuilderCodec.EntryType.IGNORE)),
|
|
Map.entry("$TODO", new BuilderCodec.KeyEntry(BuilderCodec.EntryType.IGNORE)),
|
|
Map.entry("$Author", new BuilderCodec.KeyEntry(BuilderCodec.EntryType.IGNORE)),
|
|
Map.entry("$Position", new BuilderCodec.KeyEntry(BuilderCodec.EntryType.IGNORE)),
|
|
Map.entry("$FloatingFunctionNodes", new BuilderCodec.KeyEntry(BuilderCodec.EntryType.IGNORE)),
|
|
Map.entry("$Groups", new BuilderCodec.KeyEntry(BuilderCodec.EntryType.IGNORE)),
|
|
Map.entry("$WorkspaceID", new BuilderCodec.KeyEntry(BuilderCodec.EntryType.IGNORE)),
|
|
Map.entry("$NodeEditorMetadata", new BuilderCodec.KeyEntry(BuilderCodec.EntryType.IGNORE)),
|
|
Map.entry("$NodeId", new BuilderCodec.KeyEntry(BuilderCodec.EntryType.IGNORE)),
|
|
Map.entry("Parent", new BuilderCodec.KeyEntry(BuilderCodec.EntryType.IGNORE_IN_BASE_OBJECT))
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
private S self() {
|
|
return (S)this;
|
|
}
|
|
|
|
@Nonnull
|
|
public S documentation(String doc) {
|
|
this.documentation = doc;
|
|
return this.self();
|
|
}
|
|
|
|
@Nonnull
|
|
public S versioned() {
|
|
this.versioned = true;
|
|
return this.self();
|
|
}
|
|
|
|
@Nonnull
|
|
@Deprecated
|
|
public S legacyVersioned() {
|
|
this.versioned = true;
|
|
this.useLegacyVersion = true;
|
|
return this.self();
|
|
}
|
|
|
|
@Nonnull
|
|
@Deprecated
|
|
public <FieldType> S addField(@Nonnull KeyedCodec<FieldType> codec, @Nonnull BiConsumer<T, FieldType> setter, @Nonnull Function<T, FieldType> getter) {
|
|
return this.addField(new BuilderField<>(codec, (t, fieldType, extraInfo) -> setter.accept(t, fieldType), (t1, extraInfo1) -> getter.apply(t1), null));
|
|
}
|
|
|
|
@Nonnull
|
|
public <FieldType> BuilderField.FieldBuilder<T, FieldType, S> append(
|
|
KeyedCodec<FieldType> codec, @Nonnull BiConsumer<T, FieldType> setter, @Nonnull Function<T, FieldType> getter
|
|
) {
|
|
return this.append(codec, (t, fieldType, extraInfo) -> setter.accept(t, fieldType), (t, extraInfo) -> getter.apply(t));
|
|
}
|
|
|
|
@Nonnull
|
|
public <FieldType> BuilderField.FieldBuilder<T, FieldType, S> append(
|
|
KeyedCodec<FieldType> codec, TriConsumer<T, FieldType, ExtraInfo> setter, BiFunction<T, ExtraInfo, FieldType> getter
|
|
) {
|
|
return new BuilderField.FieldBuilder<>(this.self(), codec, setter, getter, null);
|
|
}
|
|
|
|
@Nonnull
|
|
public <FieldType> BuilderField.FieldBuilder<T, FieldType, S> appendInherited(
|
|
KeyedCodec<FieldType> codec, @Nonnull BiConsumer<T, FieldType> setter, @Nonnull Function<T, FieldType> getter, @Nonnull BiConsumer<T, T> inherit
|
|
) {
|
|
return this.appendInherited(
|
|
codec,
|
|
(t, fieldType, extraInfo) -> setter.accept(t, fieldType),
|
|
(t, extraInfo) -> getter.apply(t),
|
|
(t, parent, extraInfo) -> inherit.accept(t, parent)
|
|
);
|
|
}
|
|
|
|
@Nonnull
|
|
public <FieldType> BuilderField.FieldBuilder<T, FieldType, S> appendInherited(
|
|
KeyedCodec<FieldType> codec,
|
|
TriConsumer<T, FieldType, ExtraInfo> setter,
|
|
BiFunction<T, ExtraInfo, FieldType> getter,
|
|
TriConsumer<T, T, ExtraInfo> inherit
|
|
) {
|
|
return new BuilderField.FieldBuilder<>(this.self(), codec, setter, getter, inherit);
|
|
}
|
|
|
|
@Nonnull
|
|
public <FieldType> S addField(@Nonnull BuilderField<T, FieldType> entry) {
|
|
if (entry.getMinVersion() > entry.getMaxVersion()) {
|
|
throw new IllegalArgumentException("Min version must be less than the max version: " + entry);
|
|
} else {
|
|
List<BuilderField<T, ?>> fields = this.entries.computeIfAbsent(entry.getCodec().getKey(), k -> new ObjectArrayList());
|
|
|
|
for (BuilderField<T, ?> field : fields) {
|
|
if (entry.getMaxVersion() >= field.getMinVersion() && entry.getMinVersion() <= field.getMaxVersion()) {
|
|
throw new IllegalArgumentException("Field already defined for this version range!");
|
|
}
|
|
}
|
|
|
|
fields.add(entry);
|
|
this.stringTreeMap.put(entry.getCodec().getKey(), new BuilderCodec.KeyEntry<>(fields));
|
|
return this.self();
|
|
}
|
|
}
|
|
|
|
@Nonnull
|
|
public S afterDecode(@Nonnull Consumer<T> afterDecode) {
|
|
Objects.requireNonNull(afterDecode, "afterDecodeAndValidate can't be null!");
|
|
return this.afterDecode((t, extraInfo) -> afterDecode.accept(t));
|
|
}
|
|
|
|
@Nonnull
|
|
public S afterDecode(BiConsumer<T, ExtraInfo> afterDecode) {
|
|
this.afterDecode = Objects.requireNonNull(afterDecode, "afterDecodeAndValidate can't be null!");
|
|
return this.self();
|
|
}
|
|
|
|
@Nonnull
|
|
@Deprecated
|
|
public S validator(BiConsumer<T, ValidationResults> validator) {
|
|
this.validator = Objects.requireNonNull(validator, "validator can't be null!");
|
|
return this.self();
|
|
}
|
|
|
|
@Nonnull
|
|
public S metadata(Metadata metadata) {
|
|
if (this.metadata == null) {
|
|
this.metadata = new ObjectArrayList();
|
|
}
|
|
|
|
this.metadata.add(metadata);
|
|
return this.self();
|
|
}
|
|
|
|
@Nonnull
|
|
public S codecVersion(int minCodecVersion, int codecVersion) {
|
|
this.minCodecVersion = minCodecVersion;
|
|
this.codecVersion = codecVersion;
|
|
return this.self();
|
|
}
|
|
|
|
@Nonnull
|
|
public S codecVersion(int codecVersion) {
|
|
this.minCodecVersion = 0;
|
|
this.codecVersion = codecVersion;
|
|
return this.self();
|
|
}
|
|
|
|
@Nonnull
|
|
public BuilderCodec<T> build() {
|
|
return new BuilderCodec<>(this);
|
|
}
|
|
}
|
|
|
|
protected static enum EntryType {
|
|
FIELD,
|
|
IGNORE,
|
|
IGNORE_IN_BASE_OBJECT;
|
|
}
|
|
|
|
protected static class KeyEntry<T> {
|
|
private final BuilderCodec.EntryType type;
|
|
@Nullable
|
|
private final List<BuilderField<T, ?>> fields;
|
|
|
|
public KeyEntry(BuilderCodec.EntryType type) {
|
|
this.type = type;
|
|
this.fields = null;
|
|
}
|
|
|
|
public KeyEntry(List<BuilderField<T, ?>> fields) {
|
|
this.type = BuilderCodec.EntryType.FIELD;
|
|
this.fields = fields;
|
|
}
|
|
|
|
public BuilderCodec.EntryType getType() {
|
|
return this.type;
|
|
}
|
|
|
|
@Nullable
|
|
public List<BuilderField<T, ?>> getFields() {
|
|
return this.fields;
|
|
}
|
|
}
|
|
}
|