Skip to content

Commit c7d2f98

Browse files
lazarev-pvPavel Lazarev
and
Pavel Lazarev
authored
#52: Add generator of constants with names of fields (#57)
The PR contains implementation of what was suggested in the issue #52. There are some limitations described in `FieldGeneratorAnnotationProcessor`'s javadocs. Right now it supports only Java classes and records. Support of Kt data-classes could be added if we are ok with the current approach --------- Co-authored-by: Pavel Lazarev <[email protected]>
1 parent 977e279 commit c7d2f98

31 files changed

+1526
-0
lines changed

ext-meta-generator/README.md

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
Add annotation process to the compilation stage. Example for maven:
2+
```xml
3+
<plugin>
4+
<groupId>org.apache.maven.plugins</groupId>
5+
<artifactId>maven-compiler-plugin</artifactId>
6+
<version>3.11.0</version>
7+
<configuration>
8+
<debug>true</debug>
9+
<annotationProcessorPaths>
10+
<annotationProcessorPath>
11+
<groupId>tech.ydb.yoj</groupId>
12+
<artifactId>yoj-ext-meta-generator</artifactId>
13+
<version>${yoj.version}</version>
14+
</annotationProcessorPath>
15+
</annotationProcessorPaths>
16+
<annotationProcessors>
17+
<annotationProcessor>tech.ydb.yoj.generator.FieldGeneratorAnnotationProcessor</annotationProcessor>
18+
</annotationProcessors>
19+
</configuration>
20+
</plugin>
21+
```
22+
The annotation will process classes annotated with @Table such that
23+
```java
24+
package some.pack;
25+
26+
@Table(name = "audit_event_record")
27+
public class TypicalEntity {
28+
@Column
29+
private final Id id;
30+
31+
public static class Id {
32+
private final String topicName;
33+
private final int topicPartition;
34+
private final long offset;
35+
}
36+
@Nullable
37+
private final Instant lastUpdated;
38+
}
39+
```
40+
and will generate meta-classes like:
41+
```java
42+
package some.pack.generated;
43+
44+
public class TypicalEntityFields {
45+
public static final String LAST_UPDATED = "lastUpdated";
46+
public class Id {
47+
public static final String TOPIC_NAME = "id.topicName";
48+
public static final String TOPIC_PARTITION = "id.topicPartition";
49+
public static final String OFFSET = "id.offset";
50+
}
51+
}
52+
```

ext-meta-generator/pom.xml

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<artifactId>yoj-ext-meta-generator</artifactId>
8+
<packaging>jar</packaging>
9+
10+
<parent>
11+
<groupId>tech.ydb.yoj</groupId>
12+
<artifactId>yoj-parent</artifactId>
13+
<version>2.2.10-SNAPSHOT</version>
14+
</parent>
15+
16+
<name>YOJ - Meta generator</name>
17+
<description>
18+
Module that contains generators of classes with meta-info such as FieldGeneratorAnnotationProcessor
19+
</description>
20+
<dependencies>
21+
<dependency>
22+
<groupId>com.google.guava</groupId>
23+
<artifactId>guava</artifactId>
24+
</dependency>
25+
<dependency>
26+
<groupId>tech.ydb.yoj</groupId>
27+
<artifactId>yoj-repository</artifactId>
28+
</dependency>
29+
<dependency>
30+
<groupId>org.apache.logging.log4j</groupId>
31+
<artifactId>log4j-slf4j-impl</artifactId>
32+
</dependency>
33+
<dependency>
34+
<groupId>org.apache.logging.log4j</groupId>
35+
<artifactId>log4j-core</artifactId>
36+
</dependency>
37+
38+
<dependency>
39+
<groupId>javax.annotation</groupId>
40+
<artifactId>javax.annotation-api</artifactId>
41+
<scope>test</scope>
42+
</dependency>
43+
<dependency>
44+
<groupId>com.google.testing.compile</groupId>
45+
<artifactId>compile-testing</artifactId>
46+
<scope>test</scope>
47+
</dependency>
48+
<dependency>
49+
<groupId>com.github.tschuchortdev</groupId>
50+
<artifactId>kotlin-compile-testing</artifactId>
51+
<scope>test</scope>
52+
<exclusions>
53+
<exclusion>
54+
<groupId>org.jetbrains</groupId>
55+
<artifactId>annotations</artifactId>
56+
</exclusion>
57+
</exclusions>
58+
</dependency>
59+
</dependencies>
60+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
package tech.ydb.yoj.generator;
2+
3+
import org.slf4j.Logger;
4+
import org.slf4j.LoggerFactory;
5+
import tech.ydb.yoj.ExperimentalApi;
6+
import tech.ydb.yoj.databind.schema.Table;
7+
8+
import javax.annotation.processing.AbstractProcessor;
9+
import javax.annotation.processing.RoundEnvironment;
10+
import javax.annotation.processing.SupportedAnnotationTypes;
11+
import javax.annotation.processing.SupportedSourceVersion;
12+
import javax.lang.model.SourceVersion;
13+
import javax.lang.model.element.Element;
14+
import javax.lang.model.element.ElementKind;
15+
import javax.lang.model.element.PackageElement;
16+
import javax.lang.model.element.TypeElement;
17+
import javax.tools.FileObject;
18+
import java.io.IOException;
19+
import java.io.Writer;
20+
import java.util.Set;
21+
22+
/**
23+
* Generates a "-Fields" classes for all entities in a module.
24+
* <p>This is useful for building queries like '<code>where(AuditEvent.Id.TRAIL_ID).eq(trailId)</code>'</p>
25+
* <p>Assume that we have an entity:</p>
26+
* <pre>{@code
27+
* @Table(...)
28+
* class MyTable {
29+
* String strField;
30+
* Object anyTypeField;
31+
* Id idField;
32+
* OtherClass fieldOfOtherClass;
33+
*
34+
* static class Id {
35+
* String value;
36+
* OneMoreLevel deepField1;
37+
* OneMoreLevel deepField2;
38+
*
39+
* static class OneMoreLevel {
40+
* Integer field;
41+
* }
42+
* }
43+
* }
44+
* }
45+
* A generated class will be
46+
* <pre>{@code
47+
* class MyTableFields { // Annotation processors must create a new class, so the name is different
48+
* // fields considered as 'simple' if there is no nested class of the field's type
49+
* public static final String STR_FIELD = "strField";
50+
* public static final String ANY_TYPE_FIELD = "anyTypeField";
51+
* // `fieldOfOtherClass` is a simple field because `OtherClass` is not inside the given class
52+
* public static final String FIELD_OF_OTHER_CLASS = "fieldOfOtherClass";
53+
*
54+
* // idField is not simple because it has a type Id and there is a nested class Id
55+
* // Pay attention that the generated nested class uses the field's name! It's necessary because we might
56+
* // have several fields of the same type
57+
* public static class IdField {
58+
* public static final String VALUE = "idField.value"; // Mind that the value has "idField." prefix
59+
*
60+
* // The Annotation Processor works recursively
61+
* // Also it's the example of several fields with the same type
62+
* static class DeepField1 {
63+
* public static final String FIELD = "idField.deepField1.field";
64+
* }
65+
* static class DeepField2 {
66+
* // Pay attention that ".deepField2." is used here
67+
* public static final String FIELD = "idField.deepField2.field";
68+
* }
69+
* }
70+
* }
71+
* }
72+
* <ul>
73+
* <b>Additional info:</b>
74+
* <li>Support Records</li>
75+
* <li>Support Kotlin data classes</li>
76+
* </ul>
77+
* <ul>
78+
* <b>Known issues (should be fixed in future):</b>
79+
* <li>if entity doesn't have @Table it won't be processed even if it's implements Entity interface</li>
80+
* <li>We assume that annotation @Table is used on top-level class</li>
81+
* <li>The AP will break in case of two nested classes which refer each other (i.e. circular dependency) </li>
82+
* <li>Will generate nested classes even disregarding @Column's flatten option </li>
83+
* <li>No logs are written</li>
84+
* <li>if a field has type of a class which is not nested inside the annotated class, the field will be ignored</li>
85+
* <li>There is a rare situation when generated code won't compile. The following source class
86+
* <pre>{@code
87+
* class Name{
88+
* Class1 nameClash;
89+
* public static class Class1 {
90+
* Class2 nameClash;
91+
* class Class2{
92+
* String nameClash;
93+
* }
94+
* }
95+
* }
96+
* }
97+
* will produce
98+
* <pre>{@code
99+
* public class NameFields {
100+
* public class NameClash {
101+
* public class NameClash {
102+
* public static final String NAME_CLASH = "nameClash.nameClash.nameClash";
103+
* }
104+
* }
105+
* }
106+
* }
107+
* which won't compile due to 2 NameClash classes.
108+
* </li>
109+
* </ul>
110+
*
111+
* @author pavel-lazarev
112+
*/
113+
114+
@ExperimentalApi(issue = "https://github.com/ydb-platform/yoj-project/pull/57")
115+
@SupportedAnnotationTypes({
116+
"tech.ydb.yoj.databind.schema.Table",
117+
})
118+
@SupportedSourceVersion(SourceVersion.RELEASE_17)
119+
public class FieldGeneratorAnnotationProcessor extends AbstractProcessor {
120+
121+
private static final Logger log = LoggerFactory.getLogger(FieldGeneratorAnnotationProcessor.class);
122+
123+
private static final String TARGET_PACKAGE = "generated";
124+
private static final String TARGET_CLASS_NAME_SUFFIX = "Fields";
125+
126+
@Override
127+
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
128+
Set<? extends Element> elementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(Table.class);
129+
log.info("Found {} classes to process ", elementsAnnotatedWith.size());
130+
for (Element rootElement : elementsAnnotatedWith) {
131+
log.debug("Processing {}", rootElement.getSimpleName());
132+
133+
SourceClassStructure sourceClassStructure = SourceClassStructure.analyse(
134+
rootElement,
135+
processingEnv.getTypeUtils()
136+
);
137+
TargetClassStructure targetClassStructure = TargetClassStructure.build(
138+
sourceClassStructure,
139+
rootElement.getSimpleName() + TARGET_CLASS_NAME_SUFFIX
140+
);
141+
142+
String packageName = Utils.concatFieldNameChain(
143+
calcPackage(rootElement),
144+
TARGET_PACKAGE
145+
);
146+
String generatedSource = StringConstantsRenderer.render(targetClassStructure, packageName);
147+
log.debug("Generated:\n {}", generatedSource);
148+
saveFile(
149+
generatedSource,
150+
Utils.concatFieldNameChain(
151+
packageName,
152+
targetClassStructure.className()
153+
)
154+
);
155+
}
156+
157+
return false;
158+
}
159+
160+
private String calcPackage(Element element) {
161+
while (element.getKind() != ElementKind.PACKAGE) {
162+
element = element.getEnclosingElement();
163+
}
164+
PackageElement packageElement = (PackageElement) element;
165+
return packageElement.getQualifiedName().toString();
166+
}
167+
168+
private void saveFile(String classContent, String fullClassName) {
169+
try {
170+
FileObject file = processingEnv.getFiler().createSourceFile(fullClassName);
171+
try (Writer writer = file.openWriter()) {
172+
writer.write(classContent);
173+
}
174+
} catch (IOException ex) {
175+
throw new RuntimeException(
176+
"Can not save file %s. Content:\n%s".formatted(fullClassName, classContent),
177+
ex
178+
);
179+
}
180+
}
181+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package tech.ydb.yoj.generator;
2+
3+
import javax.lang.model.element.Element;
4+
import javax.lang.model.element.ElementKind;
5+
import javax.lang.model.element.Modifier;
6+
import javax.lang.model.element.TypeElement;
7+
import javax.lang.model.element.VariableElement;
8+
import javax.lang.model.util.Types;
9+
import java.util.List;
10+
11+
/**
12+
* Information about all fields in a source class
13+
*
14+
* @param name the original name of a field
15+
* @param type the full type name of an original field
16+
*/
17+
record FieldInfo(String name, String type) {
18+
19+
public static List<FieldInfo> extractAllFields(Element classElement, Types typeUtils) {
20+
return classElement.getEnclosedElements().stream()
21+
.filter(FieldInfo::isFieldRelevant)
22+
.map(element -> FieldInfo.extractField(element, typeUtils))
23+
.toList();
24+
}
25+
26+
private static FieldInfo extractField(Element fieldElementName, Types typeUtils) {
27+
return new FieldInfo(
28+
fieldElementName.getSimpleName().toString(),
29+
calcType(fieldElementName, typeUtils)
30+
);
31+
}
32+
33+
/*
34+
The implementation looks like heresy, but it's necessary.
35+
If you just call `element.asType().toString()` on an element with an annotation,
36+
it will return a string with a type and the annotation's name; but we need ONLY the type
37+
*/
38+
private static String calcType(Element element, Types typeUtils) {
39+
Element nonPrimitiveType = typeUtils.asElement(element.asType());
40+
41+
if (nonPrimitiveType != null
42+
&& (
43+
nonPrimitiveType.getKind() == ElementKind.CLASS ||
44+
nonPrimitiveType.getKind() == ElementKind.RECORD
45+
)
46+
) {
47+
return ((TypeElement) nonPrimitiveType).getQualifiedName().toString();
48+
} else {
49+
// In case of primitive we don't care about type because it will never be nested
50+
return "-primitive-";
51+
}
52+
}
53+
54+
private static boolean isFieldRelevant(Element e) {
55+
if (e.getKind() != ElementKind.FIELD) {
56+
return false;
57+
}
58+
VariableElement variableElement = (VariableElement) e;
59+
//noinspection RedundantIfStatement
60+
if (
61+
variableElement.getModifiers().contains(Modifier.STATIC) ||
62+
variableElement.getModifiers().contains(Modifier.TRANSIENT)
63+
) {
64+
return false;
65+
}
66+
return true;
67+
}
68+
}

0 commit comments

Comments
 (0)