Skip to content

Commit ebbc96d

Browse files
IGNITE-26728 Perform enum fields serialization/deserialization with external mappers (#12554)
1 parent 1f1c2e1 commit ebbc96d

28 files changed

+1033
-480
lines changed
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.ignite.internal;
19+
20+
import java.lang.annotation.ElementType;
21+
import java.lang.annotation.Retention;
22+
import java.lang.annotation.RetentionPolicy;
23+
import java.lang.annotation.Target;
24+
25+
/**
26+
* Annotation used to specify a custom mapping strategy for an enum type during code generation.
27+
* It allows associating a custom mapper class that defines how enum constants are serialized and deserialized.
28+
*
29+
* <p>It is used in conjunction with {@link Order} annotation and used to mark fields of enum type
30+
* that should be serialized and deserialized using custom logic.</p>
31+
*
32+
* <p>The class specified by {@link #value()} must implement {@code org.apache.ignite.plugin.extensions.communication.mappers.CustomMapper}
33+
* interface to provide methods for converting enum values to and from their external representations.</p>
34+
*
35+
* @see #value()
36+
* @see org.apache.ignite.internal.Order
37+
*/
38+
@Retention(RetentionPolicy.CLASS)
39+
@Target(ElementType.FIELD)
40+
public @interface CustomMapper {
41+
/**
42+
* Returns the fully qualified class name of the custom mapper implementation
43+
* used to handle the enum's value conversion.
44+
*
45+
* <p>The specified class must be available on the classpath during code generation
46+
* and must adhere to the expected mapper interface contract.</p>
47+
*
48+
* @return The fully qualified name of the mapper class. Must not be null or empty.
49+
*/
50+
String value() default "";
51+
}

modules/codegen2/src/main/java/org/apache/ignite/internal/MessageProcessor.java

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,14 @@
3434
import javax.lang.model.element.Modifier;
3535
import javax.lang.model.element.TypeElement;
3636
import javax.lang.model.element.VariableElement;
37+
import javax.lang.model.type.TypeKind;
3738
import javax.lang.model.type.TypeMirror;
3839
import javax.tools.Diagnostic;
3940

41+
import org.apache.ignite.lang.IgniteBiTuple;
42+
43+
import static org.apache.ignite.internal.MessageSerializerGenerator.DLFT_ENUM_MAPPER_CLS;
44+
4045
/**
4146
* Annotation processor that generates serialization and deserialization code for classes implementing the {@code Message} interface.
4247
* <p>
@@ -71,6 +76,9 @@ public class MessageProcessor extends AbstractProcessor {
7176
/** This is the only message with zero fields. A serializer must be generated due to restrictions in our communication process. */
7277
static final String HANDSHAKE_WAIT_MESSAGE = "org.apache.ignite.spi.communication.tcp.messages.HandshakeWaitMessage";
7378

79+
/** */
80+
private final Map<String, IgniteBiTuple<String, String>> enumMappersInUse = new HashMap<>();
81+
7482
/**
7583
* Processes all classes implementing the {@code Message} interface and generates corresponding serializer code.
7684
*/
@@ -132,9 +140,11 @@ private List<VariableElement> orderedFields(TypeElement type) {
132140
if (el.getModifiers().contains(Modifier.STATIC)) {
133141
processingEnv.getMessager().printMessage(
134142
Diagnostic.Kind.ERROR,
135-
"Annotation @Order must be used only for non-static fields.",
143+
"Annotation @Order must only be used for non-static fields.",
136144
el);
137145
}
146+
147+
validateEnumFieldMapping(type, el);
138148
}
139149
}
140150

@@ -156,4 +166,54 @@ private List<VariableElement> orderedFields(TypeElement type) {
156166

157167
return result;
158168
}
169+
170+
/**
171+
* Validates consistency of enum field mappers configuration: the same mapper is used for the same enum in different messages,
172+
* CustomMapper annotation is used only for enum fields.
173+
*
174+
* @param type Type implementing Message interface.
175+
* @param el Enclosed element of the type.
176+
*/
177+
private void validateEnumFieldMapping(TypeElement type, Element el) {
178+
CustomMapper custMappAnn = el.getAnnotation(CustomMapper.class);
179+
if (isEnumField(el)) {
180+
String enumClsFullName = el.asType().toString();
181+
String enumMapperClsName = custMappAnn != null ? custMappAnn.value() : DLFT_ENUM_MAPPER_CLS;
182+
String msgClsName = type.toString();
183+
184+
IgniteBiTuple<String, String> otherMsgAndMapperClassesNames =
185+
enumMappersInUse.put(enumClsFullName, new IgniteBiTuple<>(msgClsName, enumMapperClsName));
186+
187+
if (otherMsgAndMapperClassesNames != null) {
188+
String otherMsgClsName = otherMsgAndMapperClassesNames.get1();
189+
String otherEnumMapperClsName = otherMsgAndMapperClassesNames.get2();
190+
191+
if (!otherEnumMapperClsName.equals(enumMapperClsName)) {
192+
processingEnv.getMessager().printMessage(
193+
Diagnostic.Kind.ERROR,
194+
"Enum " + enumClsFullName + " is declared with different mappers: " +
195+
otherEnumMapperClsName + " in " + otherMsgClsName + " and " +
196+
enumMapperClsName + " in " + msgClsName +
197+
". Only one mapper is allowed per enum type.",
198+
el);
199+
}
200+
}
201+
}
202+
else if (custMappAnn != null) {
203+
processingEnv.getMessager().printMessage(
204+
Diagnostic.Kind.ERROR,
205+
"Annotation @CustomMapper must only be used for enum fields.",
206+
el);
207+
}
208+
}
209+
210+
/** */
211+
private boolean isEnumField(Element el) {
212+
TypeMirror elType = el.asType();
213+
214+
if (elType.getKind() != TypeKind.DECLARED)
215+
return false;
216+
217+
return processingEnv.getTypeUtils().asElement(elType).getKind() == ElementKind.ENUM;
218+
}
159219
}

modules/codegen2/src/main/java/org/apache/ignite/internal/MessageSerializerGenerator.java

Lines changed: 140 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,12 @@ class MessageSerializerGenerator {
8383
/** */
8484
private static final String METHOD_JAVADOC = "/** */";
8585

86+
/** */
87+
private static final String RETURN_FALSE_STMT = "return false;";
88+
89+
/** */
90+
static final String DLFT_ENUM_MAPPER_CLS = "org.apache.ignite.plugin.extensions.communication.mappers.DefaultEnumMapper";
91+
8692
/** Collection of lines for {@code writeTo} method. */
8793
private final List<String> write = new ArrayList<>();
8894

@@ -92,6 +98,9 @@ class MessageSerializerGenerator {
9298
/** Collection of message-specific imports. */
9399
private final Set<String> imports = new TreeSet<>();
94100

101+
/** Collection of Serializer class fields containing mappers for message enum fields. */
102+
private final Set<String> fields = new TreeSet<>();
103+
95104
/** */
96105
private final ProcessingEnvironment env;
97106

@@ -142,6 +151,8 @@ private String generateSerializerCode(String serClsName) throws IOException {
142151
try (Writer writer = new StringWriter()) {
143152
writeClassHeader(writer, PKG_NAME, serClsName);
144153

154+
writeClassFields(writer);
155+
145156
// Write #writeTo method.
146157
for (String w: write)
147158
writer.write(w + NL);
@@ -238,10 +249,6 @@ private void processField(VariableElement field, int opt) throws Exception {
238249
if (assignableFrom(field.asType(), type(Throwable.class.getName())))
239250
throw new UnsupportedOperationException("You should use ErrorMessage for serialization of throwables.");
240251

241-
if (enumType(erasedType(field.asType())))
242-
throw new IllegalArgumentException("Unsupported enum type: " + field.asType() +
243-
". The enum must be wrapped into a Message (see, for example, TransactionIsolationMessage).");
244-
245252
writeField(field, opt);
246253
readField(field, opt);
247254
}
@@ -396,6 +403,45 @@ else if (assignableFrom(erasedType(type), type(Collection.class.getName()))) {
396403
"MessageCollectionItemType." + messageCollectionItemType(typeArgs.get(0)));
397404
}
398405

406+
else if (enumType(type)) {
407+
Element element = env.getTypeUtils().asElement(type);
408+
imports.add(element.toString());
409+
410+
String enumName = element.getSimpleName().toString();
411+
String enumFieldPrefix = typeNameToFieldName(enumName);
412+
413+
String mapperCallStmnt;
414+
415+
CustomMapper custMapperAnn = field.getAnnotation(CustomMapper.class);
416+
417+
if (custMapperAnn != null) {
418+
String fullMapperName = custMapperAnn.value();
419+
if (fullMapperName == null || fullMapperName.isEmpty())
420+
throw new IllegalArgumentException("Please specify a not-null not-empty EnumMapper class name");
421+
422+
imports.add("org.apache.ignite.plugin.extensions.communication.mappers.EnumMapper");
423+
imports.add(fullMapperName);
424+
425+
String simpleName = fullMapperName.substring(fullMapperName.lastIndexOf('.') + 1);
426+
427+
String mapperFieldName = enumFieldPrefix + "Mapper";
428+
429+
fields.add("private final EnumMapper<" + enumName + "> " + mapperFieldName + " = new " + simpleName + "();");
430+
431+
mapperCallStmnt = mapperFieldName + ".encode";
432+
}
433+
else {
434+
imports.add(DLFT_ENUM_MAPPER_CLS);
435+
String enumValuesFieldName = enumFieldPrefix + "Vals";
436+
437+
fields.add("private final " + enumName + "[] " + enumValuesFieldName + " = " + enumName + ".values();");
438+
439+
mapperCallStmnt = "DefaultEnumMapper.INSTANCE.encode";
440+
}
441+
442+
returnFalseIfEnumWriteFailed(write, "writer.writeByte", mapperCallStmnt, getExpr);
443+
}
444+
399445
else
400446
throw new IllegalArgumentException("Unsupported declared type: " + type);
401447

@@ -405,6 +451,15 @@ else if (assignableFrom(erasedType(type), type(Collection.class.getName()))) {
405451
throw new IllegalArgumentException("Unsupported type kind: " + type.getKind());
406452
}
407453

454+
/**
455+
* Converts type name to camel case field name. Example: {@code "MyType"} -> {@code "myType"}.
456+
*/
457+
private String typeNameToFieldName(String typeName) {
458+
char[] typeNameChars = typeName.toCharArray();
459+
typeNameChars[0] = Character.toLowerCase(typeNameChars[0]);
460+
return new String(typeNameChars);
461+
}
462+
408463
/**
409464
* Generate code of writing single field:
410465
* <pre>
@@ -419,7 +474,24 @@ private void returnFalseIfWriteFailed(Collection<String> code, String accessor,
419474

420475
indent++;
421476

422-
code.add(line("return false;"));
477+
code.add(line(RETURN_FALSE_STMT));
478+
479+
indent--;
480+
}
481+
482+
/**
483+
* Generate code of writing single enum field mapped with EnumMapper:
484+
* <pre>
485+
* if (!writer.writeByte(myEnumMapper.encode(msg.myEnum()))
486+
* return false;
487+
* </pre>
488+
*/
489+
private void returnFalseIfEnumWriteFailed(Collection<String> code, String writerCall, String mapperCall, String fieldGetterCall) {
490+
code.add(line("if (!%s(%s(msg.%s)))", writerCall, mapperCall, fieldGetterCall));
491+
492+
indent++;
493+
494+
code.add(line(RETURN_FALSE_STMT));
423495

424496
indent--;
425497
}
@@ -538,6 +610,17 @@ else if (assignableFrom(erasedType(type), type(Collection.class.getName()))) {
538610
"MessageCollectionItemType." + messageCollectionItemType(typeArgs.get(0)));
539611
}
540612

613+
else if (enumType(type)) {
614+
String fieldPrefix = typeNameToFieldName(env.getTypeUtils().asElement(type).getSimpleName().toString());
615+
616+
boolean hasCustMapperAnn = field.getAnnotation(CustomMapper.class) != null;
617+
618+
String mapperCallStmnt = hasCustMapperAnn ? fieldPrefix + "Mapper.decode" : "DefaultEnumMapper.INSTANCE.decode";
619+
String enumValsFieldName = hasCustMapperAnn ? null : fieldPrefix + "Vals";
620+
621+
returnFalseIfEnumReadFailed(name, mapperCallStmnt, enumValsFieldName);
622+
}
623+
541624
else
542625
throw new IllegalArgumentException("Unsupported declared type: " + type);
543626

@@ -646,7 +729,36 @@ private void returnFalseIfReadFailed(String var, String mtd, String... args) {
646729

647730
indent++;
648731

649-
read.add(line("return false;"));
732+
read.add(line(RETURN_FALSE_STMT));
733+
734+
indent--;
735+
}
736+
737+
/**
738+
* Generate code of reading single field:
739+
* <pre>
740+
* msg.id(reader.readInt());
741+
*
742+
* if (!reader.isLastRead())
743+
* return false;
744+
* </pre>
745+
*
746+
* @param msgSetterName Variable name.
747+
* @param mapperDecodeCallStmnt Method name.
748+
*/
749+
private void returnFalseIfEnumReadFailed(String msgSetterName, String mapperDecodeCallStmnt, String enumValuesFieldName) {
750+
if (enumValuesFieldName == null)
751+
read.add(line("msg.%s(%s(reader.readByte()));", msgSetterName, mapperDecodeCallStmnt));
752+
else
753+
read.add(line("msg.%s(%s(%s, reader.readByte()));", msgSetterName, mapperDecodeCallStmnt, enumValuesFieldName));
754+
755+
read.add(EMPTY);
756+
757+
read.add(line("if (!reader.isLastRead())"));
758+
759+
indent++;
760+
761+
read.add(line(RETURN_FALSE_STMT));
650762

651763
indent--;
652764
}
@@ -680,6 +792,24 @@ private String line(String format, Object... args) {
680792
return sb.toString();
681793
}
682794

795+
/** Write serializer class fields: enum values, custom enum mappers. */
796+
private void writeClassFields(Writer writer) throws IOException {
797+
if (fields.isEmpty())
798+
return;
799+
800+
indent = 1;
801+
802+
for (String field: fields) {
803+
writer.write(line(METHOD_JAVADOC));
804+
writer.write(NL);
805+
writer.write(line(field));
806+
writer.write(NL);
807+
}
808+
writer.write(NL);
809+
810+
indent = 0;
811+
}
812+
683813
/** Write header of serializer class: license, imports, class declaration. */
684814
private void writeClassHeader(Writer writer, String pkgName, String serClsName) throws IOException {
685815
try (InputStream in = getClass().getClassLoader().getResourceAsStream("license.txt");
@@ -701,8 +831,8 @@ private void writeClassHeader(Writer writer, String pkgName, String serClsName)
701831
imports.add("org.apache.ignite.plugin.extensions.communication.MessageWriter");
702832
imports.add("org.apache.ignite.plugin.extensions.communication.MessageReader");
703833

704-
for (String i: imports)
705-
writer.write("import " + i + ";" + NL);
834+
for (String regularImport: imports)
835+
writer.write("import " + regularImport + ";" + NL);
706836

707837
writer.write(NL);
708838
writer.write(CLS_JAVADOC);
@@ -727,13 +857,9 @@ private boolean assignableFrom(TypeMirror type, TypeMirror superType) {
727857

728858
/** */
729859
private boolean enumType(TypeMirror type) {
730-
if (type.getKind() == TypeKind.DECLARED) {
731-
Element element = env.getTypeUtils().asElement(type);
860+
Element element = env.getTypeUtils().asElement(type);
732861

733-
return element != null && element.getKind() == ElementKind.ENUM;
734-
}
735-
736-
return false;
862+
return element != null && element.getKind() == ElementKind.ENUM;
737863
}
738864

739865
/** */

modules/core/pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,10 @@
387387
<annotationProcessors>
388388
<annotationProcessor>org.apache.ignite.internal.MessageProcessor</annotationProcessor>
389389
</annotationProcessors>
390+
<compilerArgs combine.children="append">
391+
<arg>-Xmaxerrs</arg>
392+
<arg>1000</arg>
393+
</compilerArgs>
390394
</configuration>
391395
</plugin>
392396
</plugins>

0 commit comments

Comments
 (0)