diff --git a/core/src/main/java/org/infinispan/protostream/annotations/Proto.java b/core/src/main/java/org/infinispan/protostream/annotations/Proto.java
new file mode 100644
index 000000000..f7c2daee0
--- /dev/null
+++ b/core/src/main/java/org/infinispan/protostream/annotations/Proto.java
@@ -0,0 +1,21 @@
+package org.infinispan.protostream.annotations;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Defines a Protocol Buffers message without having to annotate all fields with {@link ProtoField}.
+ * Use this annotation to quickly generate messages from records or classes with public fields.
+ * Fields must be public and they will be assigned incremental numbers based on the declaration order.
+ * It is possible to override the automated defaults for a field by using the {@link ProtoField} annotation.
+ *
+ * @since 5.0
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Proto {
+}
diff --git a/core/src/main/java/org/infinispan/protostream/annotations/ProtoTypeId.java b/core/src/main/java/org/infinispan/protostream/annotations/ProtoTypeId.java
index 0a00573f4..3617524dc 100644
--- a/core/src/main/java/org/infinispan/protostream/annotations/ProtoTypeId.java
+++ b/core/src/main/java/org/infinispan/protostream/annotations/ProtoTypeId.java
@@ -7,8 +7,8 @@
import java.lang.annotation.Target;
/**
- * An optional annotation for specifying the a numeric type identifier for a Protobuf message or enum type. This numeric
- * identifier must be globally unique so it can be used to identify the type instead of the fully qualified name.
+ * An optional annotation for specifying a numeric type identifier for a Protobuf message or enum type. This numeric
+ * identifier must be globally unique, so it can be used to identify the type instead of the fully qualified name.
*
* This Java annotations results in a protostream documentation annotation 'TypeId' being added to the generated proto
* schema.
diff --git a/core/src/main/java/org/infinispan/protostream/annotations/impl/AbstractMarshallerCodeGenerator.java b/core/src/main/java/org/infinispan/protostream/annotations/impl/AbstractMarshallerCodeGenerator.java
index 4ad8902ff..e2400bba8 100644
--- a/core/src/main/java/org/infinispan/protostream/annotations/impl/AbstractMarshallerCodeGenerator.java
+++ b/core/src/main/java/org/infinispan/protostream/annotations/impl/AbstractMarshallerCodeGenerator.java
@@ -545,8 +545,8 @@ private void generateFieldReadMethod(ProtoMessageTypeMetadata messageTypeMetadat
iw.println("int $len = $in.readUInt32();");
iw.println("int $limit = $in.pushLimit($len);");
iw.println("int $t = $in.readTag();");
- String key = generateMapFieldReadMethod(mapMetadata.getKey(), iw, noFactory, true);
- String value = generateMapFieldReadMethod(mapMetadata.getValue(), iw, noFactory, false);
+ String key = generateMapFieldReadMethod(mapMetadata.getKey(), iw, true);
+ String value = generateMapFieldReadMethod(mapMetadata.getValue(), iw, false);
iw.printf("%s.put(%s, %s);\n", makeCollectionLocalVar(mapMetadata), key, value);
iw.println("$in.checkLastTagWas(0);");
iw.println("$in.popLimit($limit);");
@@ -559,11 +559,9 @@ private void generateFieldReadMethod(ProtoMessageTypeMetadata messageTypeMetadat
iw.dec().println("}");
}
- private String generateMapFieldReadMethod(ProtoFieldMetadata fieldMetadata, IndentWriter iw, boolean noFactory, boolean readNext) {
- final String v = makeFieldLocalVar(fieldMetadata);
- if (noFactory || fieldMetadata.isRepeated()) {
- iw.printf("%s %s = %s;\n", fieldMetadata.getJavaTypeName(), v, fieldMetadata.getProtobufType().getJavaType().defaultValueAsString());
- }
+ private String generateMapFieldReadMethod(ProtoFieldMetadata fieldMetadata, IndentWriter iw, boolean readNext) {
+ final String v = "__mv$" + fieldMetadata.getNumber();
+ iw.printf("%s %s = %s;\n", fieldMetadata.getJavaTypeName(), v, fieldMetadata.getProtobufType().getJavaType().defaultValueAsString());
iw.printf("if ($t == %s) {\n", makeFieldTag(fieldMetadata.getNumber(), fieldMetadata.getProtobufType().getWireType()));
iw.inc();
if (BaseProtoSchemaGenerator.generateMarshallerDebugComments) {
diff --git a/core/src/main/java/org/infinispan/protostream/annotations/impl/ProtoMessageTypeMetadata.java b/core/src/main/java/org/infinispan/protostream/annotations/impl/ProtoMessageTypeMetadata.java
index 56727e22c..62a73420d 100644
--- a/core/src/main/java/org/infinispan/protostream/annotations/impl/ProtoMessageTypeMetadata.java
+++ b/core/src/main/java/org/infinispan/protostream/annotations/impl/ProtoMessageTypeMetadata.java
@@ -19,6 +19,7 @@
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
+import org.infinispan.protostream.annotations.Proto;
import org.infinispan.protostream.annotations.ProtoComment;
import org.infinispan.protostream.annotations.ProtoFactory;
import org.infinispan.protostream.annotations.ProtoField;
@@ -170,13 +171,11 @@ public void generateProto(IndentWriter iw, ProtoSyntax syntax) {
ProtoFieldMetadata field = fieldsByNumber.get(memberNumber);
XClass where = reserved.checkReserved(memberNumber);
if (where != null) {
- throw new ProtoSchemaBuilderException("Protobuf field \"" + field.getLocation() + "\" with number " + memberNumber +
- " conflicts with 'reserved' statement in " + where.getCanonicalName());
+ throw Log.LOG.reservedNumber(memberNumber, field.getName(), where.getCanonicalName());
}
where = reserved.checkReserved(field.getName());
if (where != null) {
- throw new ProtoSchemaBuilderException("Protobuf field \"" + field.getLocation() + "\" with number " + memberNumber +
- " conflicts with 'reserved' statement in " + where.getCanonicalName());
+ throw Log.LOG.reservedName(field.getName(), where.getCanonicalName());
}
}
@@ -254,25 +253,19 @@ public void scanMemberAnnotations() {
int startPos = 0;
if (isIndexedContainer || isIterableContainer) {
if (parameterTypes.length == 0 || parameterTypes[0] != typeFactory.fromClass(int.class)) {
- throw new ProtoSchemaBuilderException("@ProtoFactory annotated " + factoryKind
- + " signature mismatch. The first parameter is expected to be of type 'int' : "
- + factory.toGenericString());
+ throw Log.LOG.factorySignatureMismatch(factoryKind, factory.toGenericString());
}
startPos = 1;
}
String[] parameterNames = factory.getParameterNames();
if (parameterNames.length != fieldsByNumber.size() + startPos) {
- throw new ProtoSchemaBuilderException("@ProtoFactory annotated " + factoryKind
- + " signature mismatch. Expected " + (fieldsByNumber.size() + startPos) + " parameters but found "
- + parameterNames.length + " : " + factory.toGenericString());
+ throw Log.LOG.factorySignatureMismatch(factoryKind, fieldsByNumber.size() + startPos, parameterNames.length, factory.toGenericString());
}
for (; startPos < parameterNames.length; startPos++) {
String parameterName = parameterNames[startPos];
ProtoFieldMetadata fieldMetadata = getFieldByPropertyName(parameterName);
if (fieldMetadata == null) {
- throw new ProtoSchemaBuilderException("@ProtoFactory annotated " + factoryKind
- + " signature mismatch. The parameter '" + parameterName
- + "' does not match any field : " + factory.toGenericString());
+ throw Log.LOG.factorySignatureMismatch(factoryKind, parameterName, factory.toGenericString());
}
XClass parameterType = parameterTypes[startPos];
boolean paramTypeMismatch = false;
@@ -289,9 +282,7 @@ public void scanMemberAnnotations() {
paramTypeMismatch = true;
}
if (paramTypeMismatch) {
- throw new ProtoSchemaBuilderException("@ProtoFactory annotated " + factoryKind
- + " signature mismatch: " + factory.toGenericString() + ". The parameter '"
- + parameterName + "' does not match the type from the field definition.");
+ throw Log.LOG.factorySignatureMismatchType(factoryKind, factory.toGenericString(), parameterName);
}
}
}
@@ -313,24 +304,28 @@ private ProtoFieldMetadata getFieldByPropertyName(String propName) {
private void checkInstantiability() {
// ensure the class is not abstract
if (annotatedClass.isAbstract() || annotatedClass.isInterface()) {
- throw new ProtoSchemaBuilderException("Abstract classes are not allowed: " + getAnnotatedClassName());
+ throw Log.LOG.abstractClassNotAllowed(getAnnotatedClassName());
}
// ensure it is not a local or anonymous class
if (annotatedClass.isLocal()) {
- throw new ProtoSchemaBuilderException("Local or anonymous classes are not allowed. The class " + getAnnotatedClassName() + " must be instantiable using an accessible no-argument constructor.");
+ throw Log.LOG.localOrAnonymousClass(getAnnotatedClassName());
}
// ensure the class is not a non-static inner class
if (annotatedClass.getEnclosingClass() != null && !annotatedClass.isStatic()) {
- throw new ProtoSchemaBuilderException("Non-static inner classes are not allowed. The class " + getAnnotatedClassName() + " must be instantiable using an accessible no-argument constructor.");
+ throw Log.LOG.nonStaticInnerClass(getAnnotatedClassName());
+ }
+ if (javaClass.isRecord()) {
+ Iterable extends XConstructor> declaredConstructors = javaClass.getDeclaredConstructors();
+ factory = declaredConstructors.iterator().next();
}
for (XConstructor c : annotatedClass.getDeclaredConstructors()) {
if (c.getAnnotation(ProtoFactory.class) != null) {
if (factory != null) {
- throw new ProtoSchemaBuilderException("Found more than one @ProtoFactory annotated method / constructor : " + c);
+ throw Log.LOG.multipleFactories(c.toString());
}
if (c.isPrivate()) {
- throw new ProtoSchemaBuilderException("@ProtoFactory annotated constructor must not be private: " + c);
+ throw Log.LOG.privateFactory(c.toString());
}
factory = c;
}
@@ -339,16 +334,16 @@ private void checkInstantiability() {
for (XMethod m : annotatedClass.getDeclaredMethods()) {
if (m.getAnnotation(ProtoFactory.class) != null) {
if (factory != null) {
- throw new ProtoSchemaBuilderException("Found more than one @ProtoFactory annotated method / constructor : " + m);
+ throw Log.LOG.multipleFactories(m.toString());
}
if (!isAdapter && !m.isStatic()) {
- throw new ProtoSchemaBuilderException("@ProtoFactory annotated method must be static: " + m);
+ throw Log.LOG.nonStaticFactory(m.toString());
}
if (m.isPrivate()) {
- throw new ProtoSchemaBuilderException("@ProtoFactory annotated method must not be private: " + m);
+ throw Log.LOG.privateFactory(m.toString());
}
if (m.getReturnType() != javaClass) {
- throw new ProtoSchemaBuilderException("@ProtoFactory annotated method has wrong return type: " + m);
+ throw Log.LOG.wrongFactoryReturnType(m.toString());
}
factory = m;
}
@@ -372,141 +367,21 @@ private void discoverFields(XClass clazz, Set examinedClasses, Map fieldsByNumber, Map fieldsByName, Set oneofs) {
Set skipMethods = new HashSet<>();
for (XMethod method : clazz.getDeclaredMethods()) {
@@ -524,14 +399,7 @@ private void discoverFields(XClass clazz, Set examinedClasses, Map 3) {
- propertyName = Character.toLowerCase(method.getName().charAt(3)) + method.getName().substring(4);
- } else {
- throw new ProtoSchemaBuilderException("Illegal setter method signature: " + method);
- }
- if (isAdapter && method.getParameterTypes().length != 2 || !isAdapter && method.getParameterTypes().length != 1) {
- throw new ProtoSchemaBuilderException("Illegal setter method signature: " + method);
- }
+ propertyName = detectPropertyNameFromSetter(method);
//TODO [anistor] also check setter args
unknownFieldSetSetter = method;
unknownFieldSetGetter = findGetter(propertyName, method.getParameterTypes()[0]);
@@ -539,16 +407,7 @@ private void discoverFields(XClass clazz, Set examinedClasses, Map 3) {
- propertyName = Character.toLowerCase(method.getName().charAt(3)) + method.getName().substring(4);
- } else if (method.getName().startsWith("is") && method.getName().length() > 2) {
- propertyName = Character.toLowerCase(method.getName().charAt(2)) + method.getName().substring(3);
- } else {
- throw new ProtoSchemaBuilderException("Illegal getter method signature: " + method);
- }
- if (isAdapter && method.getParameterTypes().length != 1 || !isAdapter && method.getParameterTypes().length != 0) {
- throw new ProtoSchemaBuilderException("Illegal getter method signature: " + method);
- }
+ propertyName = determinePropertyNameFromGetter(method);
//TODO [anistor] also check getter args
unknownFieldSetGetter = method;
unknownFieldSetSetter = findSetter(propertyName, unknownFieldSetGetter.getReturnType());
@@ -571,15 +430,7 @@ private void discoverFields(XClass clazz, Set examinedClasses, Map= 4) {
- propertyName = Character.toLowerCase(method.getName().charAt(3)) + method.getName().substring(4);
- } else {
- // not a standard java-beans setter, use the whole name as property name
- propertyName = method.getName();
- }
- if (isAdapter && method.getParameterTypes().length != 2 || !isAdapter && method.getParameterTypes().length != 1) {
- throw new ProtoSchemaBuilderException("Illegal setter method signature: " + method);
- }
+ propertyName = detectPropertyNameFromSetter(method);
//TODO [anistor] also check setter args
setter = method;
getter = findGetter(propertyName, method.getParameterTypes()[0]);
@@ -591,17 +442,7 @@ private void discoverFields(XClass clazz, Set examinedClasses, Map= 4) {
- propertyName = Character.toLowerCase(method.getName().charAt(3)) + method.getName().substring(4);
- } else if (method.getName().startsWith("is") && method.getName().length() >= 3) {
- propertyName = Character.toLowerCase(method.getName().charAt(2)) + method.getName().substring(3);
- } else {
- // not a standard java-beans getter
- propertyName = method.getName();
- }
- if (isAdapter && method.getParameterTypes().length != 1 || !isAdapter && method.getParameterTypes().length != 0) {
- throw new ProtoSchemaBuilderException("Illegal setter method signature: " + method);
- }
+ propertyName = determinePropertyNameFromGetter(method);
//TODO [anistor] also check getter args
getter = method;
getterReturnType = getter.getReturnType();
@@ -624,11 +465,7 @@ private void discoverFields(XClass clazz, Set examinedClasses, Map examinedClasses, Map examinedClasses, Map examinedClasses, Map fieldsByNumber, Map fieldsByName, Set oneofs) {
+ boolean implicitFields = clazz.getAnnotation(Proto.class) != null;
+ int position = 0;
+ for (XField field : clazz.getDeclaredFields()) {
+ position++;
+ if (field.getAnnotation(ProtoUnknownFieldSet.class) != null) {
+ if (isAdapter) {
+ throw new ProtoSchemaBuilderException("No ProtoStream annotations should be present on fields when @ProtoAdapter is present on a class : " + clazz.getCanonicalName() + '.' + field);
+ }
+ if (unknownFieldSetField != null || unknownFieldSetGetter != null || unknownFieldSetSetter != null) {
+ throw new ProtoSchemaBuilderException("The @ProtoUnknownFieldSet annotation should not occur more than once in a class and its superclasses and superinterfaces : " + clazz.getCanonicalName() + '.' + field);
+ }
+ if (field.getAnnotation(ProtoField.class) != null) {
+ throw new ProtoSchemaBuilderException("The @ProtoUnknownFieldSet and @ProtoField annotations cannot be used together: " + field);
+ }
+ unknownFieldSetField = field;
+ } else {
+ ProtoField annotation = field.getAnnotation(ProtoField.class);
+ if (annotation != null || implicitFields) {
+ validateField(clazz, field);
+ int number = annotation != null ? getNumber(annotation, field) : position;
+ String fieldName = getName(annotation, field);
+ Type protobufType = defaultType(annotation, field.getType());
+ boolean isArray = isArray(field.getType(), protobufType);
+ boolean isRepeated = isRepeated(field.getType(), protobufType);
+ boolean isRequired = annotation != null && annotation.required();
+ if (isRequired && protoSchemaGenerator.syntax() != ProtoSyntax.PROTO2) {
+ throw new ProtoSchemaBuilderException("Field '" + fieldName + "' of " + clazz.getCanonicalName() + " cannot be marked required when using \"" + protoSchemaGenerator.syntax() + "\" syntax, while processing " + this.protoSchemaGenerator.generator);
+ }
+ boolean isMap = isMap(field.getType());
+ if (isMap && protoSchemaGenerator.syntax() == ProtoSyntax.PROTO2) {
+ throw new ProtoSchemaBuilderException("Field '" + fieldName + "' of " + clazz.getCanonicalName() + " of type map is not supported when using \"" + protoSchemaGenerator.syntax() + "\" syntax, while processing " + this.protoSchemaGenerator.generator);
+ }
+ if (isRepeated && isRequired) {
+ throw new ProtoSchemaBuilderException("Repeated field '" + fieldName + "' of " + clazz.getCanonicalName() + " cannot be marked required.");
+ }
+ XClass javaType = getJavaTypeFromAnnotation(annotation);
+ if (javaType == typeFactory.fromClass(void.class)) {
+ javaType = isRepeated ? field.determineRepeatedElementType() : field.getType();
+ }
+ if (javaType == typeFactory.fromClass(byte[].class) && protobufType == Type.MESSAGE) {
+ // MESSAGE is the default and stands for 'undefined', we can override it with a better default
+ protobufType = Type.BYTES;
+ }
+ if (!javaType.isArray() && !javaType.isPrimitive() && javaType.isAbstract() && !javaType.isEnum()) {
+ throw Log.LOG.abstractType(javaType.getCanonicalName(), fieldName, clazz.getCanonicalName());
+ }
+
+ protobufType = getProtobufType(javaType, protobufType);
+
+ ProtoTypeMetadata protoTypeMetadata = null;
+ if (protobufType.getJavaType() == JavaType.ENUM || protobufType.getJavaType() == JavaType.MESSAGE) {
+ protoTypeMetadata = protoSchemaGenerator.scanAnnotations(javaType);
+ }
+
+ ProtoFieldMetadata fieldMetadata;
+ if (isMap) {
+ // Determine the map implementation
+ XClass repeatedImplementation = getMapImplementation(clazz, field.getType(), getMapImplementationFromAnnotation(annotation), fieldName, isRepeated);
+ XClass keyJavaType = field.getTypeArgument(0);
+ Type keyProtobufType = getProtobufType(keyJavaType, Type.MESSAGE);
+ if (!keyProtobufType.isValidMapKey()) {
+ throw new ProtoSchemaBuilderException("The key of the map field '" + fieldName + "' of " + clazz.getName() + " must be either a String or an integral type, while processing " + this.protoSchemaGenerator.generator);
+ }
+ fieldMetadata = new ProtoMapMetadata(number, fieldName, keyJavaType, javaType, repeatedImplementation, keyProtobufType, protobufType, protoTypeMetadata, field);
+ } else {
+ Object defaultValue = getDefaultValue(clazz, fieldName, javaType, protobufType, annotation == null ? "" : annotation.defaultValue(), isRepeated);
+ if (!isRequired && !isRepeated && javaType.isPrimitive() && defaultValue == null) {
+ throw new ProtoSchemaBuilderException("Primitive field '" + fieldName + "' of " + clazz.getCanonicalName() + " is not nullable so it should be either marked required or should have a default value, while processing " + this.protoSchemaGenerator.generator);
+ }
+ // Handle oneof
+ String oneof = validateOneOf(clazz, fieldsByName, oneofs, annotation, fieldName, isRepeated, isRequired);
+ // Determine the collection implementation
+ XClass repeatedImplementation;
+ if (isArray) {
+ repeatedImplementation = typeFactory.fromClass(ArrayList.class);
+ } else {
+ repeatedImplementation = getCollectionImplementation(clazz, field.getType(), getCollectionImplementationFromAnnotation(annotation), fieldName, isRepeated);
+ }
+ fieldMetadata = new ProtoFieldMetadata(number, fieldName, oneof, javaType, repeatedImplementation,
+ protobufType, protoTypeMetadata, isRequired, isRepeated, isArray, defaultValue, field);
+ }
+
+ ProtoFieldMetadata existing = fieldsByNumber.get(number);
+ if (existing != null) {
+ throw new ProtoSchemaBuilderException("Duplicate field number definition. Found two field definitions with number " + number + ": in "
+ + fieldMetadata.getLocation() + " and in " + existing.getLocation() + ", while processing " + this.protoSchemaGenerator.generator);
+ }
+ existing = fieldsByName.get(fieldMetadata.getName());
+ if (existing != null) {
+ throw new ProtoSchemaBuilderException("Duplicate field name definition. Found two field definitions with name '" + fieldMetadata.getName() + "': in "
+ + fieldMetadata.getLocation() + " and in " + existing.getLocation() + ", while processing " + this.protoSchemaGenerator.generator);
+ }
+
+ checkReserved(fieldMetadata);
+ fieldsByNumber.put(fieldMetadata.getNumber(), fieldMetadata);
+ fieldsByName.put(fieldName, fieldMetadata);
+ }
+ }
+ }
+ }
+
+ private static String getName(ProtoField annotation, XField field) {
+ if (annotation == null || annotation.name().isEmpty()) {
+ return field.getName();
+ } else {
+ return annotation.name();
+ }
+ }
+
+ private String validateOneOf(XClass clazz, Map fieldsByName, Set oneofs, ProtoField annotation, String fieldName, boolean isRepeated, boolean isRequired) {
+ if (annotation == null) {
+ return null;
+ }
+ String oneof = annotation.oneof();
+ if (oneof.isEmpty()) {
+ oneof = null;
+ } else {
+ if (oneof.equals(fieldName) || fieldsByName.containsKey(oneof)) {
+ throw new ProtoSchemaBuilderException("The field named '" + fieldName + "' of " + clazz.getName() + " is member of the '" + oneof + "' oneof which collides with an existing field or oneof, while processing " + this.protoSchemaGenerator.generator);
+ }
+ if (isRepeated || isRequired) {
+ throw new ProtoSchemaBuilderException("The field named '" + fieldName + "' of " + clazz.getName() + " cannot be marked repeated or required since it is member of the '" + oneof + " oneof, while processing " + this.protoSchemaGenerator.generator);
+ }
+ oneofs.add(oneof);
+ }
+ return oneof;
+ }
+
+ private void validateField(XClass clazz, XField field) {
+ if (isAdapter) {
+ throw new ProtoSchemaBuilderException("No ProtoStream annotations should be present on fields when @ProtoAdapter is present on a class : " + clazz.getCanonicalName() + '.' + field);
+ }
+ if (field.isStatic()) {
+ throw new ProtoSchemaBuilderException("Static fields cannot be @ProtoField annotated: " + clazz.getCanonicalName() + '.' + field);
+ }
+ if (factory == null && field.isFinal()) { //todo [anistor] maybe allow this
+ throw new ProtoSchemaBuilderException("Final fields cannot be @ProtoField annotated: " + clazz.getCanonicalName() + '.' + field);
+ }
+ if (field.isPrivate()) {
+ throw new ProtoSchemaBuilderException("Private fields cannot be @ProtoField annotated: " + clazz.getCanonicalName() + '.' + field);
+ }
+ }
+
+ private String detectPropertyNameFromSetter(XMethod method) {
+ if (isAdapter && method.getParameterTypes().length != 2 || !isAdapter && method.getParameterTypes().length != 1) {
+ throw new ProtoSchemaBuilderException("Illegal setter method signature: " + method);
+ }
+ if (method.getName().startsWith("set") && method.getName().length() > 3) {
+ return Character.toLowerCase(method.getName().charAt(3)) + method.getName().substring(4);
+ } else {
+ return method.getName(); // throw new ProtoSchemaBuilderException("Illegal setter method signature: " + method);
+ }
+ }
+
+ private String determinePropertyNameFromGetter(XMethod method) {
+ if (isAdapter && method.getParameterTypes().length != 1 || !isAdapter && method.getParameterTypes().length != 0) {
+ throw new ProtoSchemaBuilderException("Illegal getter method signature: " + method);
+ }
+ if (method.getName().startsWith("get") && method.getName().length() > 3) {
+ return Character.toLowerCase(method.getName().charAt(3)) + method.getName().substring(4);
+ } else if (method.getName().startsWith("is") && method.getName().length() > 2) {
+ return Character.toLowerCase(method.getName().charAt(2)) + method.getName().substring(3);
+ } else {
+ // not a standard java-beans getter
+ return method.getName(); //throw new ProtoSchemaBuilderException("Illegal getter method signature: " + method);
+ }
+ }
+
+ private void discoverFieldsFromRecord(XClass clazz, Map fieldsByNumber, Map fieldsByName) {
+ String[] parameterNames = factory.getParameterNames();
+ XClass[] parameterTypes = factory.getParameterTypes();
+ for (int i = 0; i < factory.getParameterCount(); i++) {
+ int fieldNumber = i + 1;
+ String fieldName = parameterNames[i];
+ XClass javaType = parameterTypes[i];
+ ProtoField annotation = javaType.getAnnotation(ProtoField.class);
+
+ Type protobufType = defaultType(annotation, javaType);
+
+ XMethod getter = clazz.getMethod(fieldName);
+ boolean isArray = isArray(javaType, protobufType);
+ boolean isRepeated = isRepeated(javaType, protobufType);
+ boolean isMap = isMap(javaType);
+
+ // Determine the collection/map implementation
+ XClass repeatedImplementation = null;
+ if (isMap) {
+ repeatedImplementation = getMapImplementation(clazz, javaType, getMapImplementationFromAnnotation(annotation), fieldName, true);
+ } else if (isArray) {
+ repeatedImplementation = typeFactory.fromClass(ArrayList.class);
+ } else if (isRepeated) {
+ repeatedImplementation = getCollectionImplementation(clazz, javaType, getCollectionImplementationFromAnnotation(annotation), fieldName, true);
+ }
+
+ if (isRepeated) {
+ javaType = getter.determineRepeatedElementType();
+ }
+
+ if (javaType == typeFactory.fromClass(byte[].class) && protobufType == Type.MESSAGE) {
+ // MESSAGE is the default and stands for 'undefined', we can override it with a better default
+ protobufType = Type.BYTES;
+ }
+ if (!javaType.isArray() && !javaType.isPrimitive() && javaType.isAbstract() && !javaType.isEnum()) {
+ throw new ProtoSchemaBuilderException("The type " + javaType.getCanonicalName() + " of field '" + fieldName + "' of " + clazz.getCanonicalName() + " should not be abstract.");
+ }
+
+ protobufType = getProtobufType(javaType, protobufType);
+ ProtoTypeMetadata protoTypeMetadata = null;
+ if (protobufType.getJavaType() == JavaType.ENUM || protobufType.getJavaType() == JavaType.MESSAGE) {
+ protoTypeMetadata = protoSchemaGenerator.scanAnnotations(javaType);
+ }
+ String oneof = null;
+ Object defaultValue;
+ if (annotation == null) {
+ defaultValue = getDefaultValue(clazz, fieldName, javaType, protobufType, null, false);
+ } else {
+ if (annotation.number() > 0) fieldNumber = annotation.number();
+ if (!annotation.name().isEmpty()) fieldName = annotation.name();
+ if (!annotation.oneof().isEmpty()) oneof = annotation.oneof();
+ defaultValue = getDefaultValue(clazz, fieldName, javaType, protobufType, annotation.defaultValue(), false);
+ }
+ ProtoFieldMetadata fieldMetadata;
+ if (isMap) {
+ XClass keyJavaType = getter.getTypeArgument(0);
+ Type keyType = getProtobufType(keyJavaType, Type.MESSAGE);
+ if (!keyType.isValidMapKey()) {
+ throw new ProtoSchemaBuilderException("The key of the map field '" + fieldName + "' of " + clazz.getName() + " must be either a String or an integral type, while processing " + this.protoSchemaGenerator.generator);
+ }
+ fieldMetadata = new ProtoMapMetadata(fieldNumber, fieldName, keyJavaType, javaType, repeatedImplementation, keyType, protobufType, protoTypeMetadata, fieldName, getter, getter, null);
+ } else {
+ fieldMetadata = new ProtoFieldMetadata(fieldNumber, fieldName, oneof, javaType,
+ repeatedImplementation, protobufType, protoTypeMetadata,
+ false, isRepeated, isArray, defaultValue, fieldName,
+ getter, getter, null);
+ }
+ checkReserved(fieldMetadata);
+ fieldsByNumber.put(fieldMetadata.getNumber(), fieldMetadata);
+ fieldsByName.put(fieldMetadata.getName(), fieldMetadata);
+ }
+ }
+
private static int getNumber(ProtoField annotation, XMember member) {
int number = annotation.number();
if (number == 0) {
@@ -741,15 +818,15 @@ private static void checkReserved(ProtoFieldMetadata fieldMetadata) {
}
protected XClass getCollectionImplementationFromAnnotation(ProtoField annotation) {
- return typeFactory.fromClass(annotation.collectionImplementation());
+ return annotation == null ? typeFactory.fromClass(Collection.class) : typeFactory.fromClass(annotation.collectionImplementation());
}
protected XClass getMapImplementationFromAnnotation(ProtoField annotation) {
- return typeFactory.fromClass(annotation.mapImplementation());
+ return annotation == null ? typeFactory.fromClass(Map.class) : typeFactory.fromClass(annotation.mapImplementation());
}
protected XClass getJavaTypeFromAnnotation(ProtoField annotation) {
- return typeFactory.fromClass(annotation.javaType());
+ return annotation == null ? typeFactory.fromClass(void.class) : typeFactory.fromClass(annotation.javaType());
}
/**
@@ -1157,9 +1234,10 @@ private XMethod findGetter(String propertyName, XClass propertyType) {
boolean isBoolean = propertyType == typeFactory.fromClass(boolean.class) || propertyType == typeFactory.fromClass(Boolean.class);
String methodName = (isBoolean ? "is" : "get") + Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1);
+ XMethod getter;
if (isAdapter) {
// lookup a java-bean style method first
- XMethod getter = annotatedClass.getMethod(methodName);
+ getter = annotatedClass.getMethod(methodName);
if (getter == null && isBoolean) {
// retry with 'get' instead of 'is'
methodName = "get" + Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1);
@@ -1182,10 +1260,9 @@ private XMethod findGetter(String propertyName, XClass propertyType) {
+ "' of type " + propertyType.getCanonicalName() + " in class " + getAnnotatedClassName()
+ ". The candidate method does not have a suitable return type: " + getter);
}
- return getter;
} else {
// lookup a java-bean style method first
- XMethod getter = javaClass.getMethod(methodName);
+ getter = javaClass.getMethod(methodName);
if (getter == null && isBoolean) {
// retry with 'get' instead of 'is'
methodName = "get" + Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1);
@@ -1208,15 +1285,16 @@ private XMethod findGetter(String propertyName, XClass propertyType) {
+ "' of type " + propertyType.getCanonicalName() + " in class " + javaClass.getCanonicalName()
+ ". The candidate method does not have a suitable return type: " + getter);
}
- return getter;
}
+ return getter;
}
private XMethod findSetter(String propertyName, XClass propertyType) {
String methodName = "set" + Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1);
+ XMethod setter;
if (isAdapter) {
// lookup a java-bean style method first
- XMethod setter = annotatedClass.getMethod(methodName, javaClass, propertyType);
+ setter = annotatedClass.getMethod(methodName, javaClass, propertyType);
if (setter == null) {
// try the property name directly
setter = annotatedClass.getMethod(propertyName, javaClass, propertyType);
@@ -1230,10 +1308,9 @@ private XMethod findSetter(String propertyName, XClass propertyType) {
+ "' of type " + propertyType.getCanonicalName() + " in class " + getAnnotatedClassName()
+ ". The candidate method does not have a suitable return type: " + setter);
}
- return setter;
} else {
// lookup a java-bean style method first
- XMethod setter = javaClass.getMethod(methodName, propertyType);
+ setter = javaClass.getMethod(methodName, propertyType);
if (setter == null) {
// try the property name directly
setter = javaClass.getMethod(propertyName, propertyType);
@@ -1247,8 +1324,8 @@ private XMethod findSetter(String propertyName, XClass propertyType) {
+ "' of type " + propertyType.getCanonicalName() + " in class " + getJavaClassName()
+ ". The candidate method does not have a suitable return type: " + setter);
}
- return setter;
}
+ return setter;
}
private void checkForbiddenAnnotations(XMethod m1, XMethod m2) {
diff --git a/core/src/main/java/org/infinispan/protostream/annotations/impl/types/XClass.java b/core/src/main/java/org/infinispan/protostream/annotations/impl/types/XClass.java
index 51a8616d2..4317dac8b 100644
--- a/core/src/main/java/org/infinispan/protostream/annotations/impl/types/XClass.java
+++ b/core/src/main/java/org/infinispan/protostream/annotations/impl/types/XClass.java
@@ -87,4 +87,8 @@ default boolean isAbstract() {
default boolean isInterface() {
return Modifier.isInterface(getModifiers());
}
+
+ default boolean isRecord() {
+ return isAssignableTo(Record.class);
+ }
}
diff --git a/core/src/main/java/org/infinispan/protostream/impl/Log.java b/core/src/main/java/org/infinispan/protostream/impl/Log.java
index b6fd9b032..7b224b3f4 100644
--- a/core/src/main/java/org/infinispan/protostream/impl/Log.java
+++ b/core/src/main/java/org/infinispan/protostream/impl/Log.java
@@ -90,6 +90,30 @@ default MalformedProtobufException messageTruncated() {
@Message(value = "Invalid default value for field '%s' of Java type %s from class %s: the %s enum must have a 0 value", id = 20)
ProtoSchemaBuilderException noDefaultEnum(String fieldName, String canonicalName, String canonicalName1, String fullName);
+ @Message(value = "@ProtoFactory annotated %s signature mismatch. The first parameter is expected to be of type 'int' : %s", id = 21)
+ ProtoSchemaBuilderException factorySignatureMismatch(String kind, String factory);
+
+ @Message(value = "@ProtoFactory annotated %s signature mismatch. Expected %d parameters but found %d : %s", id = 22)
+ ProtoSchemaBuilderException factorySignatureMismatch(String kind, int expected, int found, String factory);
+
+ @Message(value = "@ProtoFactory annotated %s signature mismatch. The parameter '%s' does not match any field : %s", id = 23)
+ ProtoSchemaBuilderException factorySignatureMismatch(String kind, String parameterName, String factory);
+
+ @Message(value = "@ProtoFactory annotated %s signature mismatch: %s. The parameter '%s' does not match the type from the field definition.", id = 24)
+ ProtoSchemaBuilderException factorySignatureMismatchType(String kind, String factory, String parameterName);
+
+ @Message(value = "Found more than one @ProtoFactory annotated method / constructor : %s", id = 25)
+ ProtoSchemaBuilderException multipleFactories(String s);
+
+ @Message(value = "@ProtoFactory annotated constructor must not be private: %s", id = 26)
+ ProtoSchemaBuilderException privateFactory(String s);
+
+ @Message(value = "@ProtoFactory annotated method must be static: %s", id =27)
+ ProtoSchemaBuilderException nonStaticFactory(String s);
+
+ @Message(value = "@ProtoFactory annotated method has wrong return type: %s", id = 28)
+ ProtoSchemaBuilderException wrongFactoryReturnType(String s);
+
class LogFactory {
public static Log getLog(Class> clazz) {
return Logger.getMessageLogger(Log.class, clazz.getName());
diff --git a/processor/src/main/java/org/infinispan/protostream/annotations/impl/processor/AnnotatedClassScanner.java b/processor/src/main/java/org/infinispan/protostream/annotations/impl/processor/AnnotatedClassScanner.java
index 753efbceb..c0e8cee14 100644
--- a/processor/src/main/java/org/infinispan/protostream/annotations/impl/processor/AnnotatedClassScanner.java
+++ b/processor/src/main/java/org/infinispan/protostream/annotations/impl/processor/AnnotatedClassScanner.java
@@ -30,6 +30,7 @@
import javax.tools.Diagnostic;
import org.infinispan.protostream.annotations.AutoProtoSchemaBuilder;
+import org.infinispan.protostream.annotations.Proto;
import org.infinispan.protostream.annotations.ProtoAdapter;
import org.infinispan.protostream.annotations.ProtoEnumValue;
import org.infinispan.protostream.annotations.ProtoFactory;
@@ -153,6 +154,10 @@ void discoverClasses(RoundEnvironment roundEnv) throws AnnotationProcessingExcep
visitProtoName(e);
}
+ for (Element e : roundEnv.getElementsAnnotatedWith(Proto.class)) {
+ visitProtoMessage(e);
+ }
+
for (Element e : roundEnv.getElementsAnnotatedWith(ProtoAdapter.class)) {
visitProtoAdapter(e);
}
@@ -181,6 +186,9 @@ private void visitTypeElement(TypeElement e) {
if (e.getAnnotation(ProtoName.class) != null) {
visitProtoName(e);
}
+ if (e.getAnnotation(Proto.class) != null) {
+ visitProtoMessage(e);
+ }
for (Element member : e.getEnclosedElements()) {
if (member.getAnnotation(ProtoField.class) != null) {
@@ -244,7 +252,7 @@ private void visitProtoName(Element e) {
}
private void visitProtoMessage(Element e) {
- if (e.getKind() != ElementKind.CLASS && e.getKind() != ElementKind.INTERFACE) {
+ if (e.getKind() != ElementKind.CLASS && e.getKind() != ElementKind.INTERFACE && e.getKind() != ElementKind.RECORD) {
throw new AnnotationProcessingException(e, "@ProtoMessage can only be applied to classes and interfaces.");
}
collectClasses((TypeElement) e);
diff --git a/processor/src/main/java/org/infinispan/protostream/annotations/impl/processor/CompileTimeProtoMessageTypeMetadata.java b/processor/src/main/java/org/infinispan/protostream/annotations/impl/processor/CompileTimeProtoMessageTypeMetadata.java
index ee44817e2..aeba8c165 100644
--- a/processor/src/main/java/org/infinispan/protostream/annotations/impl/processor/CompileTimeProtoMessageTypeMetadata.java
+++ b/processor/src/main/java/org/infinispan/protostream/annotations/impl/processor/CompileTimeProtoMessageTypeMetadata.java
@@ -1,5 +1,8 @@
package org.infinispan.protostream.annotations.impl.processor;
+import java.util.Collection;
+import java.util.Map;
+
import javax.lang.model.type.TypeMirror;
import org.infinispan.protostream.annotations.ProtoField;
@@ -18,19 +21,31 @@ class CompileTimeProtoMessageTypeMetadata extends ProtoMessageTypeMetadata {
@Override
protected XClass getCollectionImplementationFromAnnotation(ProtoField annotation) {
- TypeMirror typeMirror = DangerousActions.getTypeMirror(annotation, ProtoField::collectionImplementation);
- return ((MirrorTypeFactory) typeFactory).fromTypeMirror(typeMirror);
+ if (annotation == null) {
+ return typeFactory.fromClass(Collection.class);
+ } else {
+ TypeMirror typeMirror = DangerousActions.getTypeMirror(annotation, ProtoField::collectionImplementation);
+ return ((MirrorTypeFactory) typeFactory).fromTypeMirror(typeMirror);
+ }
}
@Override
protected XClass getMapImplementationFromAnnotation(ProtoField annotation) {
- TypeMirror typeMirror = DangerousActions.getTypeMirror(annotation, ProtoField::mapImplementation);
- return ((MirrorTypeFactory) typeFactory).fromTypeMirror(typeMirror);
+ if (annotation == null) {
+ return typeFactory.fromClass(Map.class);
+ } else {
+ TypeMirror typeMirror = DangerousActions.getTypeMirror(annotation, ProtoField::mapImplementation);
+ return ((MirrorTypeFactory) typeFactory).fromTypeMirror(typeMirror);
+ }
}
@Override
protected XClass getJavaTypeFromAnnotation(ProtoField annotation) {
- TypeMirror typeMirror = DangerousActions.getTypeMirror(annotation, ProtoField::javaType);
- return ((MirrorTypeFactory) typeFactory).fromTypeMirror(typeMirror);
+ if (annotation == null) {
+ return typeFactory.fromClass(void.class);
+ } else {
+ TypeMirror typeMirror = DangerousActions.getTypeMirror(annotation, ProtoField::javaType);
+ return ((MirrorTypeFactory) typeFactory).fromTypeMirror(typeMirror);
+ }
}
}
diff --git a/processor/src/test/java/org/infinispan/protostream/annotations/impl/processor/tests/ProtoSchemaTest.java b/processor/src/test/java/org/infinispan/protostream/annotations/impl/processor/tests/ProtoSchemaTest.java
index 830e9c7cc..82a7908cb 100644
--- a/processor/src/test/java/org/infinispan/protostream/annotations/impl/processor/tests/ProtoSchemaTest.java
+++ b/processor/src/test/java/org/infinispan/protostream/annotations/impl/processor/tests/ProtoSchemaTest.java
@@ -23,6 +23,7 @@
import org.infinispan.protostream.SerializationContextInitializer;
import org.infinispan.protostream.WrappedMessage;
import org.infinispan.protostream.annotations.AutoProtoSchemaBuilder;
+import org.infinispan.protostream.annotations.Proto;
import org.infinispan.protostream.annotations.ProtoComment;
import org.infinispan.protostream.annotations.ProtoFactory;
import org.infinispan.protostream.annotations.ProtoField;
@@ -33,6 +34,12 @@
import org.infinispan.protostream.annotations.ProtoSyntax;
import org.infinispan.protostream.annotations.impl.processor.tests.testdomain.SimpleClass;
import org.infinispan.protostream.annotations.impl.processor.tests.testdomain.SimpleEnum;
+import org.infinispan.protostream.annotations.impl.processor.tests.testdomain.SimpleRecord;
+import org.infinispan.protostream.descriptors.Descriptor;
+import org.infinispan.protostream.descriptors.FieldDescriptor;
+import org.infinispan.protostream.descriptors.Label;
+import org.infinispan.protostream.descriptors.MapDescriptor;
+import org.infinispan.protostream.descriptors.Type;
import org.junit.Test;
public class ProtoSchemaTest {
@@ -136,6 +143,7 @@ public static class EmbeddedLifespanExpirableMetadata extends EmbeddedMetadata {
// EmbeddedMetadata.class,
EmbeddedMetadata.EmbeddedLifespanExpirableMetadata.class,
SimpleEnum.class,
+ SimpleRecord.class,
// String.class,
X.class
}
@@ -1376,6 +1384,54 @@ public void testGenericMessage() throws Exception {
assertEquals("asdfg", ((GenericMessage.OtherMessage) genericMessage.field4.getValue()).field1);
}
+ @Proto
+ static final class BareMessage {
+ public int anInt;
+ public String aString;
+ public List things;
+ public Map moreThings;
+ }
+
+ @ProtoSchema(schemaFileName = "bare_message.proto",
+ includeClasses = {
+ BareMessage.class,
+ }, syntax = ProtoSyntax.PROTO3
+ )
+ interface TestBareMessageSerializationContextInitializer extends GeneratedSchema {
+ }
+
+ @Test
+ public void testBareMessage() {
+ SerializationContext ctx = ProtobufUtil.newSerializationContext();
+
+ GeneratedSchema generatedSchema = new TestBareMessageSerializationContextInitializerImpl();
+ generatedSchema.registerSchema(ctx);
+ generatedSchema.registerMarshallers(ctx);
+
+ assertTrue(generatedSchema.getProtoFile().contains("message BareMessage"));
+ Descriptor message = (Descriptor) ctx.getDescriptorByName("BareMessage");
+ FieldDescriptor field = message.getFields().get(0);
+ assertEquals("anInt", field.getName());
+ assertEquals(1, field.getNumber());
+ assertEquals(Type.INT32, field.getType());
+ assertEquals(Label.OPTIONAL, field.getLabel());
+ field = message.getFields().get(1);
+ assertEquals("aString", field.getName());
+ assertEquals(2, field.getNumber());
+ assertEquals(Type.STRING, field.getType());
+ assertEquals(Label.OPTIONAL, field.getLabel());
+ field = message.getFields().get(2);
+ assertEquals("things", field.getName());
+ assertEquals(3, field.getNumber());
+ assertEquals(Type.STRING, field.getType());
+ assertEquals(Label.REPEATED, field.getLabel());
+ MapDescriptor map = (MapDescriptor) message.getFields().get(3);
+ assertEquals("moreThings", map.getName());
+ assertEquals(4, map.getNumber());
+ assertEquals(Type.STRING, map.getKeyType());
+ assertEquals(Type.STRING, map.getType());
+ }
+
//todo warnings logged to log4j during generation do not end up in compiler's message log
//todo provide a sensible value() alias for all @ProtoXyz annotations
diff --git a/processor/src/test/java/org/infinispan/protostream/annotations/impl/processor/tests/testdomain/SimpleClass.java b/processor/src/test/java/org/infinispan/protostream/annotations/impl/processor/tests/testdomain/SimpleClass.java
index 0f0b3a224..6d2a9a867 100644
--- a/processor/src/test/java/org/infinispan/protostream/annotations/impl/processor/tests/testdomain/SimpleClass.java
+++ b/processor/src/test/java/org/infinispan/protostream/annotations/impl/processor/tests/testdomain/SimpleClass.java
@@ -28,6 +28,9 @@ public class SimpleClass {
@ProtoField(number = 314, name = "my_enum_field", defaultValue = "AX")
public SimpleEnum myEnumField;
+ @ProtoField(number = 400, name ="my_record")
+ public SimpleRecord rec;
+
//TODO here we have several cases not covered by tests...
/*
private Integer x;
diff --git a/processor/src/test/java/org/infinispan/protostream/annotations/impl/processor/tests/testdomain/SimpleRecord.java b/processor/src/test/java/org/infinispan/protostream/annotations/impl/processor/tests/testdomain/SimpleRecord.java
new file mode 100644
index 000000000..e2bfbd038
--- /dev/null
+++ b/processor/src/test/java/org/infinispan/protostream/annotations/impl/processor/tests/testdomain/SimpleRecord.java
@@ -0,0 +1,10 @@
+package org.infinispan.protostream.annotations.impl.processor.tests.testdomain;
+
+import java.util.List;
+import java.util.Map;
+
+import org.infinispan.protostream.annotations.Proto;
+
+@Proto
+public record SimpleRecord(String aString, int anInt, SimpleEnum anEnum, List things, float[] someFloats, Map aMapOfStrings) {
+}