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 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) { +}