hytale-server/com/hypixel/hytale/codec/builder/BuilderCodec.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;
}
}
}