Skip to content

Commit 9ead7d5

Browse files
committed
Allow TypeCodes to specify their max length
1 parent 9324a60 commit 9ead7d5

File tree

11 files changed

+168
-41
lines changed

11 files changed

+168
-41
lines changed
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* Copyright (c) 2024 GeyserMC
3+
* Licensed under the MIT license
4+
* @link https://github.com/GeyserMC/DatabaseUtils
5+
*/
6+
package org.geysermc.databaseutils.processor;
7+
8+
import java.io.IOException;
9+
import java.util.HashMap;
10+
import java.util.Map;
11+
import java.util.Properties;
12+
import java.util.UUID;
13+
import javax.annotation.processing.Filer;
14+
import javax.lang.model.type.TypeMirror;
15+
import javax.tools.StandardLocation;
16+
import org.geysermc.databaseutils.meta.Length;
17+
import org.geysermc.databaseutils.processor.util.TypeUtils;
18+
19+
public class CustomLengthManager {
20+
private final String filePath = "org/geysermc/databaseutils/processor/length-annotated.properties";
21+
private final TypeUtils typeUtils;
22+
private final Filer filer;
23+
24+
private final Map<String, Integer> buildInMaxLengths = new HashMap<>();
25+
private final Map<String, Integer> readMaxLengths = new HashMap<>();
26+
private final Map<String, Integer> maxLengths = new HashMap<>();
27+
28+
public CustomLengthManager(TypeUtils typeUtils, Filer filer) {
29+
this.typeUtils = typeUtils;
30+
this.filer = filer;
31+
buildInMaxLengths.put(UUID.class.getCanonicalName(), 16);
32+
}
33+
34+
public void read() {
35+
try {
36+
var properties = new Properties();
37+
try (var stream = getClass().getClassLoader().getResourceAsStream(filePath)) {
38+
if (stream == null) {
39+
return;
40+
}
41+
properties.load(stream);
42+
}
43+
properties.stringPropertyNames().forEach(key -> {
44+
readMaxLengths.put(key, Integer.parseInt(properties.getProperty(key)));
45+
});
46+
} catch (IOException e) {
47+
throw new RuntimeException(e);
48+
}
49+
}
50+
51+
public void customLength(TypeMirror mirror, Length length) {
52+
maxLengths.put(typeUtils.canonicalName(mirror).toString(), length.max());
53+
}
54+
55+
public Integer maxLength(TypeMirror mirror) {
56+
return maxLength(typeUtils.canonicalName(mirror));
57+
}
58+
59+
public Integer maxLength(CharSequence name) {
60+
var nameString = name.toString();
61+
var maxLength = buildInMaxLengths.get(nameString);
62+
if (maxLength != null) return maxLength;
63+
maxLength = maxLengths.get(nameString);
64+
if (maxLength != null) return maxLength;
65+
return readMaxLengths.get(nameString);
66+
}
67+
68+
public void write() {
69+
if (maxLengths.isEmpty()) {
70+
return;
71+
}
72+
73+
try {
74+
var properties = new Properties();
75+
maxLengths.forEach((k, v) -> properties.setProperty(k, v.toString()));
76+
var resource = filer.createResource(StandardLocation.CLASS_OUTPUT, "", filePath);
77+
try (var writer = resource.openWriter()) {
78+
properties.store(writer, null);
79+
}
80+
} catch (IOException e) {
81+
throw new RuntimeException(e);
82+
}
83+
}
84+
}

ap/src/main/java/org/geysermc/databaseutils/processor/EntityManager.java

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,23 @@
2323
import org.geysermc.databaseutils.meta.Entity;
2424
import org.geysermc.databaseutils.meta.Index;
2525
import org.geysermc.databaseutils.meta.Key;
26+
import org.geysermc.databaseutils.meta.Length;
2627
import org.geysermc.databaseutils.processor.info.ColumnInfo;
2728
import org.geysermc.databaseutils.processor.info.EntityInfo;
2829
import org.geysermc.databaseutils.processor.info.IndexInfo;
2930
import org.geysermc.databaseutils.processor.info.IndexInfo.IndexType;
3031
import org.geysermc.databaseutils.processor.type.LimitsEnforcer;
32+
import org.geysermc.databaseutils.processor.util.InvalidRepositoryException;
3133
import org.geysermc.databaseutils.processor.util.TypeUtils;
3234

3335
final class EntityManager {
3436
private final Map<CharSequence, EntityInfo> entityInfoByClassName = new HashMap<>();
3537
private final TypeUtils typeUtils;
38+
private final CustomLengthManager lengthManager;
3639

37-
EntityManager(final TypeUtils typeUtils) {
40+
EntityManager(TypeUtils typeUtils, CustomLengthManager lengthManager) {
3841
this.typeUtils = Objects.requireNonNull(typeUtils);
42+
this.lengthManager = Objects.requireNonNull(lengthManager);
3943
}
4044

4145
Collection<EntityInfo> processedEntities() {
@@ -88,8 +92,25 @@ EntityInfo processEntity(TypeMirror typeMirror) {
8892
if (field.getModifiers().contains(Modifier.STATIC)) {
8993
continue;
9094
}
95+
var fieldType = typeUtils.toBoxedTypeElement(field.asType());
96+
97+
// length which will be used for table creation and limit enforcement
98+
var length = field.getAnnotation(Length.class);
99+
var maxLengthColumn = -1;
100+
//noinspection ConstantValue you still have to validate it
101+
if (length != null && length.max() > 0) {
102+
maxLengthColumn = length.max();
103+
}
104+
var maxLengthType = lengthManager.maxLength(fieldType.getQualifiedName());
105+
if (maxLengthType != null && maxLengthColumn != -1) {
106+
// todo technically its an invalid Entity, make an error type for that
107+
throw new InvalidRepositoryException(
108+
"@Length was already provided by a TypeCodec for column %s. A column cannot override that.",
109+
field.getSimpleName());
110+
}
111+
var maxLength = maxLengthType != null ? maxLengthType : maxLengthColumn;
91112

92-
columns.add(new ColumnInfo(field.getSimpleName(), typeUtils.toBoxedTypeElement(field.asType()), field));
113+
columns.add(new ColumnInfo(field.getSimpleName(), fieldType, field, maxLength));
93114

94115
if (hasAnnotation(field, Key.class)) {
95116
keys.add(field.getSimpleName());

ap/src/main/java/org/geysermc/databaseutils/processor/RepositoryProcessor.java

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,12 @@
2727
import javax.lang.model.element.ElementKind;
2828
import javax.lang.model.element.ExecutableElement;
2929
import javax.lang.model.element.TypeElement;
30+
import javax.lang.model.type.DeclaredType;
3031
import javax.lang.model.type.TypeMirror;
3132
import javax.tools.Diagnostic;
3233
import org.geysermc.databaseutils.IRepository;
34+
import org.geysermc.databaseutils.codec.TypeCodec;
35+
import org.geysermc.databaseutils.meta.Length;
3336
import org.geysermc.databaseutils.meta.Query;
3437
import org.geysermc.databaseutils.meta.Repository;
3538
import org.geysermc.databaseutils.processor.action.ActionRegistry;
@@ -42,22 +45,24 @@
4245
@AutoService(Processor.class)
4346
public final class RepositoryProcessor extends AbstractProcessor {
4447
private TypeUtils typeUtils;
45-
private EntityManager entityManager;
4648
private Filer filer;
49+
private CustomLengthManager lengthManager;
50+
private EntityManager entityManager;
4751
private Messager messager;
4852

4953
@Override
5054
public synchronized void init(ProcessingEnvironment processingEnv) {
5155
super.init(processingEnv);
5256
this.typeUtils = new TypeUtils(processingEnv.getTypeUtils(), processingEnv.getElementUtils());
53-
this.entityManager = new EntityManager(typeUtils);
5457
this.filer = processingEnv.getFiler();
58+
this.lengthManager = new CustomLengthManager(typeUtils, filer);
59+
this.entityManager = new EntityManager(typeUtils, lengthManager);
5560
this.messager = processingEnv.getMessager();
5661
}
5762

5863
@Override
5964
public Set<String> getSupportedAnnotationTypes() {
60-
return Set.of(Repository.class.getCanonicalName());
65+
return Set.of(Repository.class.getCanonicalName(), Length.class.getCanonicalName());
6166
}
6267

6368
@Override
@@ -66,6 +71,38 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
6671
return true;
6772
}
6873

74+
lengthManager.read();
75+
76+
// register TypeCodecs first, for their potential Length annotation
77+
for (Element element : env.getElementsAnnotatedWith(Length.class)) {
78+
// only classes need to be processed, but field is valid
79+
if (element.getKind() != ElementKind.CLASS) {
80+
if (element.getKind() == ElementKind.FIELD) {
81+
continue;
82+
}
83+
error(element, "Only classes can be annotated with @Length");
84+
continue;
85+
}
86+
87+
var type = (TypeElement) element;
88+
DeclaredType codecElement = null;
89+
for (TypeMirror anInterface : type.getInterfaces()) {
90+
if (!typeUtils.isType(TypeCodec.class, anInterface)) {
91+
continue;
92+
}
93+
codecElement = MoreTypes.asDeclared(anInterface);
94+
}
95+
if (codecElement == null) {
96+
error(element, "Only TypeCodec classes can be annotated with @Length");
97+
continue;
98+
}
99+
100+
var codecType = codecElement.getTypeArguments().get(0);
101+
lengthManager.customLength(codecType, element.getAnnotation(Length.class));
102+
}
103+
104+
lengthManager.write();
105+
69106
List<List<RepositoryGenerator>> results = new ArrayList<>();
70107
for (int i = 0; i < RegisteredGenerators.generatorCount(); i++) {
71108
results.add(new ArrayList<>());

ap/src/main/java/org/geysermc/databaseutils/processor/info/ColumnInfo.java

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,31 +5,21 @@
55
*/
66
package org.geysermc.databaseutils.processor.info;
77

8-
import java.lang.annotation.Annotation;
98
import javax.lang.model.element.Name;
109
import javax.lang.model.element.TypeElement;
1110
import javax.lang.model.element.VariableElement;
1211
import javax.lang.model.type.TypeMirror;
1312
import org.geysermc.databaseutils.meta.Length;
1413

15-
public record ColumnInfo(Name name, TypeElement type, VariableElement variable) {
14+
/**
15+
* @param maxLength Returns the max length as provided by {@link Length} (either directly or from a TypeCodec), or -1 if no limit is provided
16+
*/
17+
public record ColumnInfo(Name name, TypeElement type, VariableElement variable, int maxLength) {
1618
public TypeMirror asType() {
1719
return type.asType();
1820
}
1921

2022
public Name typeName() {
2123
return type.getQualifiedName();
2224
}
23-
24-
public <T extends Annotation> T annotation(Class<T> annotationClass) {
25-
return variable.getAnnotation(annotationClass);
26-
}
27-
28-
/**
29-
* Returns the max length as provided by {@link Length}, or -1 if no limit is provided
30-
*/
31-
public int maxLength() {
32-
var length = annotation(Length.class);
33-
return length != null ? length.max() : -1;
34-
}
3525
}

ap/src/main/java/org/geysermc/databaseutils/processor/type/LimitsEnforcer.java

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
import java.util.List;
1414
import java.util.Map;
1515
import org.geysermc.databaseutils.DatabaseType;
16-
import org.geysermc.databaseutils.meta.Length;
1716
import org.geysermc.databaseutils.processor.info.ColumnInfo;
1817
import org.geysermc.databaseutils.processor.info.EntityInfo;
1918
import org.geysermc.databaseutils.processor.info.IndexInfo;
@@ -132,13 +131,7 @@ private int rowLengthFor(List<ColumnInfo> columns, DialectLimits limits) {
132131
return columns.stream()
133132
.mapToInt(column -> {
134133
var typeLimit = limits.limit(column.typeName());
135-
var length = column.annotation(Length.class);
136-
// todo allow TypeCodecs to specify the max length, instead of having to specify
137-
// it on the fields using the codecs
138-
if (length != null) {
139-
return typeLimit.validateAndReturnColumnLength(length.max(), column.name());
140-
}
141-
return typeLimit.validateAndReturnColumnLength(null, column.name());
134+
return typeLimit.validateAndReturnColumnLength(column.maxLength(), column.name());
142135
})
143136
.sum();
144137
}
@@ -469,8 +462,8 @@ public int maxVaryingLength() {
469462
return maxVaryingLength == UNLIMITED ? Integer.MAX_VALUE : maxVaryingLength;
470463
}
471464

472-
public int validateAndReturnColumnLength(Integer selfDefinedLength, CharSequence columnName) {
473-
if (selfDefinedLength == null && varying()) {
465+
public int validateAndReturnColumnLength(int selfDefinedLength, CharSequence columnName) {
466+
if (selfDefinedLength == -1 && varying()) {
474467
throw new InvalidRepositoryException(
475468
"Expected %s to have a Length annotation specifying the max length", columnName);
476469
}

ap/src/test/resources/test/advanced/TestEntity.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@
99
@Index(columns = {"c"})
1010
@Entity("hello")
1111
public record TestEntity(
12-
@Key int a, @Key @Length(max = 50) String b, @Length(max = 10) String c, @Length(max = 16) UUID d) {}
12+
@Key int a, @Key @Length(max = 50) String b, @Length(max = 10) String c, UUID d) {}

ap/src/test/resources/test/basic/TestEntity.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@
99
@Index(columns = {"c"})
1010
@Entity("hello")
1111
public record TestEntity(
12-
@Key int a, @Key @Length(max = 50) String b, @Length(max = 10) String c, @Length(max = 16) UUID d) {}
12+
@Key int a, @Key @Length(max = 50) String b, @Length(max = 10) String c, UUID d) {}

build.gradle.kts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ subprojects {
3131
spotless {
3232
java {
3333
palantirJavaFormat()
34-
formatAnnotations()
3534
}
3635
ratchetFrom("origin/main")
3736
}

core/src/main/java/org/geysermc/databaseutils/codec/UuidCodec.java

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@
66
package org.geysermc.databaseutils.codec;
77

88
import java.nio.ByteBuffer;
9-
import java.nio.ByteOrder;
109
import java.util.UUID;
10+
import org.geysermc.databaseutils.meta.Length;
1111

12+
@Length(max = 16)
1213
final class UuidCodec implements TypeCodec<UUID> {
1314
static final UuidCodec INSTANCE = new UuidCodec();
1415

@@ -38,10 +39,7 @@ public byte[] encode(UUID input) {
3839
}
3940

4041
byte[] uuidBytes = new byte[16];
41-
ByteBuffer.wrap(uuidBytes)
42-
.order(ByteOrder.BIG_ENDIAN)
43-
.putLong(input.getMostSignificantBits())
44-
.putLong(input.getLeastSignificantBits());
42+
ByteBuffer.wrap(uuidBytes).putLong(input.getMostSignificantBits()).putLong(input.getLeastSignificantBits());
4543
return uuidBytes;
4644
}
4745
}

core/src/main/java/org/geysermc/databaseutils/meta/Length.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,18 @@
99
import java.lang.annotation.Target;
1010
import org.checkerframework.checker.index.qual.Positive;
1111

12-
@Target(ElementType.FIELD)
12+
/**
13+
* Specifies the max length (in bytes) that the given column can be. See {@link #max()} for more information.
14+
* This annotation is mostly meant for columns of an entity, but can also be specified on a TypeCodec so that all
15+
* usages inherit this value.
16+
*/
17+
@Target({ElementType.FIELD, ElementType.TYPE})
1318
public @interface Length {
1419
/**
1520
* The maximum length (in bytes) that the given column can be. Note that for strings this mean that you have to
1621
* keep the chosen charset (by default almost always UTF-8) into account. A single UTF-8 character can vary from 1
1722
* to 4 bytes per character.
1823
*/
19-
@Positive int max() default 0;
24+
@Positive
25+
int max() default 0;
2026
}

0 commit comments

Comments
 (0)