diff --git a/helper/helper-common/src/main/java/com/linkedin/avroutil1/compatibility/CodeTransformationUtils.java b/helper/helper-common/src/main/java/com/linkedin/avroutil1/compatibility/CodeTransformationUtils.java new file mode 100644 index 000000000..ce5a6c721 --- /dev/null +++ b/helper/helper-common/src/main/java/com/linkedin/avroutil1/compatibility/CodeTransformationUtils.java @@ -0,0 +1,267 @@ +/* + * Copyright 2022 LinkedIn Corp. + * Licensed under the BSD 2-Clause License (the "License"). + * See License in the project root for license information. + */ + +package com.linkedin.avroutil1.compatibility; + +public class CodeTransformationUtils { + private CodeTransformationUtils() { + //util class + } + + /** + * Constants for Java numeric type names that are used in multiple places + */ + public static final String JAVA_LANG_INTEGER = "java.lang.Integer"; + public static final String JAVA_LANG_LONG = "java.lang.Long"; + + /** + * Finds the index position of the end of a code block by counting braces. + * Used to find the end of methods, classes, etc. + * + * @param code The source code string + * @param startPosition The position from which to start looking (usually after the opening brace) + * @return The position of the end of the block (after the closing brace) or -1 if the block end can't be found + */ + public static int findEndOfBlock(String code, int startPosition) { + if (startPosition < 0 || startPosition >= code.length()) { + return -1; // Invalid start position + } + + // If we're not starting at a brace, find the first opening brace + int position = startPosition; + if (code.charAt(position) != '{') { + position = code.indexOf('{', position); + if (position == -1) { + return -1; // No opening brace found + } + } + + // Start counting from after the opening brace + position++; + + int braceCount = 1; // We've already seen the opening brace + while (position < code.length() && braceCount > 0) { + char c = code.charAt(position); + if (c == '{') { + braceCount++; + } else if (c == '}') { + braceCount--; + } + position++; + } + + // If brace count is not 0, we didn't find the matching closing brace + return braceCount == 0 ? position : -1; + } + + /** + * Generates Javadoc for an overloaded numeric setter method or builder setter method. + * + * @param fieldName Name of the field being modified + * @param sourceType The type of the parameter (e.g. "long" or "java.lang.Long") + * @param targetType The type of the field (e.g. "int" or "java.lang.Integer") + * @param isBuilderMethod Whether this is for a builder method (returns builder) or a regular setter (returns void) + * @return StringBuilder containing the generated Javadoc + */ + public static StringBuilder generateNumericConversionJavadoc(String fieldName, String sourceType, String targetType, boolean isBuilderMethod) { + StringBuilder javadoc = new StringBuilder("\n\n /**\n"); + javadoc.append(" * Sets ").append(fieldName).append(" to the specified value.\n"); + + // Add description based on source and target types + if ("int".equals(sourceType) || JAVA_LANG_INTEGER.equals(sourceType)) { + javadoc.append(" * Accepts an "); + javadoc.append("int".equals(sourceType) ? "int" : "Integer"); + javadoc.append(" value and converts it to "); + javadoc.append("long".equals(targetType) ? "long" : "Long"); + javadoc.append(".\n"); + } else if ("long".equals(sourceType) || JAVA_LANG_LONG.equals(sourceType)) { + javadoc.append(" * Accepts a "); + javadoc.append("long".equals(sourceType) ? "long" : "Long"); + javadoc.append(" value and converts it to "); + javadoc.append("int".equals(targetType) ? "int" : "Integer"); + javadoc.append(" with bounds checking.\n"); + } + + // Add parameter description + javadoc.append(" * @param value The "); + javadoc.append("int".equals(sourceType) ? "int" : + "long".equals(sourceType) ? "long" : + JAVA_LANG_INTEGER.equals(sourceType) ? "Integer" : "Long"); + javadoc.append(" value to set\n"); + + // Add return value description for builder methods + if (isBuilderMethod) { + javadoc.append(" * @return This builder\n"); + } + + // Add exception for bounds checking (int/Integer target types) + if ("int".equals(targetType) || JAVA_LANG_INTEGER.equals(targetType)) { + javadoc.append(" * @throws org.apache.avro.AvroRuntimeException if the value is outside the "); + javadoc.append("int".equals(targetType) ? "int" : "Integer"); + javadoc.append(" range\n"); + } + + javadoc.append(" */\n"); + return javadoc; + } + + /** + * Generates the method signature for an overloaded numeric setter or builder setter. + * + * @param methodName The name of the method (e.g., "setIntField") + * @param sourceType The source type of the parameter (e.g., "long" or "java.lang.Long") + * @param paramName The name of the parameter (usually "value") + * @param returnType The return type, "void" for regular setters or builder class name for builder setters + * @return StringBuilder containing the generated method signature including the opening brace + */ + public static StringBuilder generateNumericMethodSignature( + String methodName, String sourceType, String paramName, String returnType) { + StringBuilder signature = new StringBuilder(" public "); + signature.append(returnType).append(" ") + .append(methodName).append("(") + .append(sourceType).append(" ") + .append(paramName).append(") {\n"); + return signature; + } + + /** + * Generates code for converting between numeric types with appropriate bounds checking and null handling. + * + * @param sourceType The source type parameter (e.g., "long", "java.lang.Long") + * @param targetType The target type field (e.g., "int", "java.lang.Integer") + * @param isBuilderMethod Whether this is for a builder method (returns the builder) or regular setter + * @param methodName The name of the original setter method (used for delegating to original setter) + * @param builderMethodName For builder methods, the name of the method to return to (usually same as methodName) + * @return StringBuilder containing the generated method body without the closing brace + */ + public static StringBuilder generateNumericConversionCode( + String sourceType, String targetType, + boolean isBuilderMethod, String methodName, String builderMethodName) { + StringBuilder code = new StringBuilder(); + + if ("int".equals(targetType) && "long".equals(sourceType)) { + // Convert long to int with bounds check + code.append(" if (value <= Integer.MAX_VALUE && value >= Integer.MIN_VALUE) {\n"); + if (isBuilderMethod) { + code.append(" return ").append(builderMethodName).append("((int) value);\n"); + } else { + code.append(" ").append(methodName).append("((int) value);\n"); + } + code.append(" } else {\n"); + code.append(" throw new org.apache.avro.AvroRuntimeException(\"Long value \" + value + \" cannot be cast to int\");\n"); + code.append(" }"); + } else if ("long".equals(targetType) && "int".equals(sourceType)) { + // Convert int to long + if (isBuilderMethod) { + code.append(" return ").append(builderMethodName).append("((long) value);"); + } else { + code.append(" ").append(methodName).append("((long) value);"); + } + } else if (JAVA_LANG_INTEGER.equals(targetType) && JAVA_LANG_LONG.equals(sourceType)) { + // Convert Long to Integer with bounds check and null handling + code.append(" if (value == null) {\n"); + if (isBuilderMethod) { + code.append(" return ").append(builderMethodName).append("((").append(JAVA_LANG_INTEGER).append(") null);\n"); + } else { + code.append(" ").append(methodName).append("((").append(JAVA_LANG_INTEGER).append(") null);\n"); + } + code.append(" } else if (value <= Integer.MAX_VALUE && value >= Integer.MIN_VALUE) {\n"); + if (isBuilderMethod) { + code.append(" return ").append(builderMethodName).append("(value.intValue());\n"); + } else { + code.append(" ").append(methodName).append("(value.intValue());\n"); + } + code.append(" } else {\n"); + code.append(" throw new org.apache.avro.AvroRuntimeException(\"Long value \" + value + \" cannot be cast to Integer\");\n"); + code.append(" }"); + } else if (JAVA_LANG_LONG.equals(targetType) && JAVA_LANG_INTEGER.equals(sourceType)) { + // Convert Integer to Long with null handling + code.append(" if (value == null) {\n"); + if (isBuilderMethod) { + code.append(" return ").append(builderMethodName).append("((").append(JAVA_LANG_LONG).append(") null);\n"); + } else { + code.append(" ").append(methodName).append("((").append(JAVA_LANG_LONG).append(") null);\n"); + } + code.append(" } else {\n"); + if (isBuilderMethod) { + code.append(" return ").append(builderMethodName).append("(value.longValue());\n"); + } else { + code.append(" ").append(methodName).append("(value.longValue());\n"); + } + code.append(" }"); + } + + return code; + } + + /** + * Determines the appropriate overloaded method signature for a given field type. + * + * @param fieldType The type of the field (e.g., "int", "java.lang.Integer") + * @param methodName The setter method name + * @return The signature of the overloaded method, or null if no overloaded method is needed + */ + public static String determineOverloadSignature(String fieldType, String methodName) { + String overloadSignature = null; + if ("int".equals(fieldType)) { + overloadSignature = "public void " + methodName + "(long "; + } else if ("long".equals(fieldType)) { + overloadSignature = "public void " + methodName + "(int "; + } else if (JAVA_LANG_INTEGER.equals(fieldType)) { + overloadSignature = "public void " + methodName + "(java.lang.Long "; + } else if (JAVA_LANG_LONG.equals(fieldType)) { + overloadSignature = "public void " + methodName + "(java.lang.Integer "; + } + return overloadSignature; + } + + /** + * Generates the complete code for an overloaded setter method with proper type conversion. + * + * @param methodName The name of the setter method + * @param fieldName The name of the field being set + * @param fieldType The type of the field (e.g., "int", "java.lang.Integer") + * @param isBuilderMethod Whether this is for a builder method + * @param builderReturnType If isBuilderMethod is true, the return type for the builder method + * @return Complete code for the overloaded setter method + */ + public static StringBuilder generateOverloadedSetter( + String methodName, String fieldName, String fieldType, + boolean isBuilderMethod, String builderReturnType) { + + StringBuilder overloadedSetter = new StringBuilder(); + String returnType = isBuilderMethod ? builderReturnType : "void"; + String sourceType; + String targetType = fieldType; + + // Determine the appropriate source type based on the target field type + if ("int".equals(fieldType)) { + sourceType = "long"; + } else if ("long".equals(fieldType)) { + sourceType = "int"; + } else if (JAVA_LANG_INTEGER.equals(fieldType)) { + sourceType = JAVA_LANG_LONG; + } else if (JAVA_LANG_LONG.equals(fieldType)) { + sourceType = JAVA_LANG_INTEGER; + } else { + return overloadedSetter; // Empty if not a numeric type we handle + } + + // Generate the method Javadoc + overloadedSetter.append(generateNumericConversionJavadoc(fieldName, sourceType, targetType, isBuilderMethod)); + + // Generate the method signature + overloadedSetter.append(generateNumericMethodSignature(methodName, sourceType, "value", returnType)); + + // Generate the conversion code - pass methodName as both the setter method name and builder method name + overloadedSetter.append(generateNumericConversionCode(sourceType, targetType, isBuilderMethod, methodName, methodName)); + + // Close the method + overloadedSetter.append("\n }"); + + return overloadedSetter; + } +} diff --git a/helper/helper-common/src/main/java/com/linkedin/avroutil1/compatibility/CodeTransformations.java b/helper/helper-common/src/main/java/com/linkedin/avroutil1/compatibility/CodeTransformations.java index 2e2f04593..f1c8d1647 100644 --- a/helper/helper-common/src/main/java/com/linkedin/avroutil1/compatibility/CodeTransformations.java +++ b/helper/helper-common/src/main/java/com/linkedin/avroutil1/compatibility/CodeTransformations.java @@ -6,8 +6,6 @@ package com.linkedin.avroutil1.compatibility; -import org.apache.commons.text.StringEscapeUtils; - import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -19,6 +17,7 @@ import java.util.StringJoiner; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.apache.commons.text.StringEscapeUtils; /** @@ -100,6 +99,12 @@ public class CodeTransformations { private static final String ENUM_CLASS_BODY_POST19_TEMPLATE = TemplateUtil.loadTemplate("avroutil1/templates/EnumClass19.template"); private static final String ENUM_CLASS_NO_NAMESPACE_BODY_POST19_TEMPLATE = TemplateUtil.loadTemplate("avroutil1/templates/EnumClassNoNamespace19.template"); + private static final Pattern PUT_METHOD_PATTERN = Pattern.compile( + "public\\s+void\\s+put\\s*\\(\\s*int\\s+field\\$\\s*,\\s*java\\.lang\\.Object\\s+value\\$\\s*\\)\\s*\\{\\s*" + + "switch\\s*\\(\\s*field\\$\\s*\\)\\s*\\{\\s*" + + "([^}]+)" + // Capture the case statements + "\\}\\s*\\}"); + private static final int MAX_STRING_LITERAL_SIZE = 65000; //just under 64k private CodeTransformations() { @@ -131,6 +136,11 @@ public static String applyAll( fixed = CodeTransformations.transformParseCalls(fixed, generatedBy, minSupportedVersion, maxSupportedVersion, alternativeAvsc, importStatements); fixed = CodeTransformations.addGetClassSchemaMethod(fixed, generatedBy, minSupportedVersion, maxSupportedVersion); + // Allow int fields to be set using longs, and vice versa + fixed = CodeTransformations.enhanceNumericPutMethod(fixed); + fixed = CodeTransformations.addOverloadedNumericSetterMethods(fixed); + fixed = CodeTransformations.addOverloadedNumericConstructor(fixed); + //1.6+ features if (minSupportedVersion.earlierThan(AvroVersion.AVRO_1_6)) { //optionally strip out builders @@ -141,6 +151,7 @@ public static String applyAll( fixed = CodeTransformations.removeReferencesToNewClassesFromBuilders(fixed, generatedBy, minSupportedVersion, maxSupportedVersion); } + fixed = CodeTransformations.addOverloadedNumericBuilderSetterMethods(fixed); //1.7+ features //this is considered harmless enough we can keep doing it? fixed = CodeTransformations.removeAvroGeneratedAnnotation(fixed, minSupportedVersion, maxSupportedVersion); @@ -919,6 +930,584 @@ private static String fixBuilderConstructors(String code) { " " + BUILDER_INSTANCE_NAME + " = new " + builderClassName + "(false);\n" + code.substring(insertPosition); } + /** + * Transforms the put method in Avro-generated record classes to allow setting Long values into int fields + * and Integer values into long fields. This improves type compatibility when working with numeric fields. + * This method preserves existing case statements and only enhances the ones for numeric fields. + * + * @param code generated code + * @return transformed code with enhanced put method + */ + public static String enhanceNumericPutMethod(String code) { + if (code == null || code.isEmpty()) { + return code; + } + + // Pattern to match the put method with simple casts + Matcher matcher = PUT_METHOD_PATTERN.matcher(code); + if (!matcher.find()) { + return code; // No matching put method found + } + + String caseStatements = matcher.group(1); + + // Pattern to find int and long field cases + Pattern casePattern = Pattern.compile("case\\s+(\\d+)\\s*:\\s*(\\w+)\\s*=\\s*\\(([^)]+)\\)value\\$\\s*;\\s*break\\s*;"); + Matcher caseMatcher = casePattern.matcher(caseStatements); + + // Map to store field info: caseNumber -> [fieldName, fieldType] + Map fieldInfo = new HashMap<>(); // fieldName -> type + + // Find all numeric field cases + while (caseMatcher.find()) { + String caseNumber = caseMatcher.group(1); + String fieldName = caseMatcher.group(2); + String fieldType = caseMatcher.group(3); + + if (CodeTransformationUtils.JAVA_LANG_INTEGER.equals(fieldType) || "int".equals(fieldType) || + CodeTransformationUtils.JAVA_LANG_LONG.equals(fieldType) || "long".equals(fieldType)) { + fieldInfo.put(caseNumber, new String[]{fieldName, fieldType}); + } + } + + if (fieldInfo.isEmpty()) { + return code; // No numeric fields found + } + + // Build the enhanced put method, preserving existing case statements + StringBuilder enhancedPutMethod = new StringBuilder(); + enhancedPutMethod.append("public void put(int field$, java.lang.Object value$) {\n"); + enhancedPutMethod.append(" switch (field$) {\n"); + + // Reset the matcher to process all case statements + caseMatcher = Pattern.compile("case\\s+(\\d+)\\s*:([^;]*);\\s*break\\s*;").matcher(caseStatements); + int lastMatchEnd = 0; + + while (caseMatcher.find()) { + String caseNumber = caseMatcher.group(1); + + // If this is a numeric field case that we want to enhance + if (fieldInfo.containsKey(caseNumber)) { + String[] fieldData = fieldInfo.get(caseNumber); + String fieldName = fieldData[0]; + String fieldType = fieldData[1]; + + enhancedPutMethod.append(" case ").append(caseNumber).append(": "); + + if ("int".equals(fieldType)) { + enhancedPutMethod.append("if (value$ instanceof java.lang.Long) {\n"); + enhancedPutMethod.append(" if ((java.lang.Long)value$ <= Integer.MAX_VALUE && (java.lang.Long)value$ >= Integer.MIN_VALUE) {\n"); + enhancedPutMethod.append(" this.").append(fieldName).append(" = ((java.lang.Long)value$).intValue();\n"); + enhancedPutMethod.append(" } else {\n"); + enhancedPutMethod.append(" throw new org.apache.avro.AvroRuntimeException(\"Long value \" + value$ + \" cannot be cast to int\");\n"); + enhancedPutMethod.append(" }\n"); + enhancedPutMethod.append(" } else {\n"); + enhancedPutMethod.append(" this.").append(fieldName).append(" = (java.lang.Integer)value$;\n"); + enhancedPutMethod.append(" }\n"); + enhancedPutMethod.append(" break;\n"); + } else if ("long".equals(fieldType)) { + enhancedPutMethod.append("if (value$ instanceof java.lang.Integer) {\n"); + enhancedPutMethod.append(" this.").append(fieldName).append(" = ((java.lang.Integer)value$).longValue();\n"); + enhancedPutMethod.append(" } else {\n"); + enhancedPutMethod.append(" this.").append(fieldName).append(" = (java.lang.Long)value$;\n"); + enhancedPutMethod.append(" }\n"); + enhancedPutMethod.append(" break;\n"); + } else if (CodeTransformationUtils.JAVA_LANG_INTEGER.equals(fieldType)) { + enhancedPutMethod.append("if (value$ instanceof java.lang.Long) {\n"); + enhancedPutMethod.append(" if ((java.lang.Long)value$ <= Integer.MAX_VALUE && (java.lang.Long)value$ >= Integer.MIN_VALUE) {\n"); + enhancedPutMethod.append(" this.").append(fieldName).append(" = ((java.lang.Long)value$).intValue();\n"); + enhancedPutMethod.append(" } else {\n"); + enhancedPutMethod.append(" throw new org.apache.avro.AvroRuntimeException(\"Long value \" + value$ + \" cannot be cast to Integer\");\n"); + enhancedPutMethod.append(" }\n"); + enhancedPutMethod.append(" } else {\n"); + enhancedPutMethod.append(" this.").append(fieldName).append(" = (java.lang.Integer)value$;\n"); + enhancedPutMethod.append(" }\n"); + enhancedPutMethod.append(" break;\n"); + } else if (CodeTransformationUtils.JAVA_LANG_LONG.equals(fieldType)) { + enhancedPutMethod.append("if (value$ instanceof java.lang.Integer) {\n"); + enhancedPutMethod.append(" this.").append(fieldName).append(" = ((java.lang.Integer)value$).longValue();\n"); + enhancedPutMethod.append(" } else {\n"); + enhancedPutMethod.append(" this.").append(fieldName).append(" = (java.lang.Long)value$;\n"); + enhancedPutMethod.append(" }\n"); + enhancedPutMethod.append(" break;\n"); + } + } else { + // For non-numeric fields, preserve the original case statement + enhancedPutMethod.append(" case ").append(caseNumber).append(":") + .append(caseMatcher.group(2)).append(";") + .append(" break;\n"); + } + + lastMatchEnd = caseMatcher.end(); + } + + // Add any remaining case statements (like default case) + if (lastMatchEnd < caseStatements.length()) { + String remaining = caseStatements.substring(lastMatchEnd).trim(); + if (!remaining.isEmpty()) { + enhancedPutMethod.append(" ").append(remaining).append("\n"); + } + } else { + // Add the default case if it wasn't in the original + enhancedPutMethod.append(" default: throw new IndexOutOfBoundsException(\"Invalid index: \" + field$);\n"); + } + + enhancedPutMethod.append(" }\n}"); + + // Replace the original put method with the enhanced one + return code.substring(0, matcher.start()) + enhancedPutMethod + code.substring(matcher.end()); + } + + /** + * Adds overloaded setter methods for numeric fields to allow setting Integer fields with Long values + * and Long fields with Integer values. This improves type compatibility when working with numeric fields. + * This method identifies existing setter methods and adds appropriate overloaded versions in a single pass. + * + * @param code generated code + * @return transformed code with overloaded numeric setter methods + */ + public static String addOverloadedNumericSetterMethods(String code) { + if (code == null || code.isEmpty()) { + return code; + } + + StringBuilder result = new StringBuilder(code); + + // Unified pattern to match all numeric setter methods in a single pass + Pattern numericSetterPattern = Pattern.compile( + "public\\s+void\\s+(set\\w+)\\s*\\(\\s*(int|long|java\\.lang\\.Integer|java\\.lang\\.Long)\\s+([\\w$]+)\\s*\\)\\s*\\{"); + Matcher numericSetterMatcher = numericSetterPattern.matcher(code); + + // Track positions to insert overloaded methods + // Use a map of position -> insertion text to collect all insertions + Map insertions = new HashMap<>(); + + // Process all numeric setters in a single pass + while (numericSetterMatcher.find()) { + String methodName = numericSetterMatcher.group(1); + String paramType = numericSetterMatcher.group(2); + + // Derive the field name from the method name instead of parameter name + // Convert "setFieldName" to "fieldName" (remove "set" prefix and lowercase first char) + String fieldName = methodName.substring(3, 4).toLowerCase() + methodName.substring(4); + + // Find the end of the setter method + int methodBodyStart = code.indexOf("{", numericSetterMatcher.start()); + if (methodBodyStart == -1) continue; + + int methodEnd = CodeTransformationUtils.findEndOfBlock(code, methodBodyStart); + if (methodEnd == -1) continue; + + // Determine if we need to generate an overload based on the parameter type + String overloadSignature = CodeTransformationUtils.determineOverloadSignature(paramType, methodName); + + // Skip if no overload is needed or if this overload already exists + if (overloadSignature == null || code.contains(overloadSignature)) { + continue; + } + + // Generate the complete overloaded setter method + StringBuilder overload = CodeTransformationUtils.generateOverloadedSetter( + methodName, fieldName, paramType, false, null); + + // Schedule the insertion + insertions.put(methodEnd, overload.toString()); + } + + // Apply all insertions in reverse order to maintain correct positions + List positions = new ArrayList<>(insertions.keySet()); + Collections.sort(positions, Collections.reverseOrder()); + + for (int position : positions) { + result.insert(position, insertions.get(position)); + } + + return result.toString(); + } + + /** + * Adds an overloaded constructor that allows Integer parameters for Long fields and Long parameters for Integer fields. + * This makes the generated code more flexible by allowing automatic type conversion between numeric types. + * The implementation handles both boxed Integer/Long types and primitive int/long types, with proper null handling + * for boxed types and default values for primitives when null is passed. + * + * @param code generated code + * @return transformed code with overloaded constructor for numeric type conversions + */ + public static String addOverloadedNumericConstructor(String code) { + if (code == null || code.isEmpty()) { + return code; + } + + // First, identify all numeric fields in the class (both primitive and boxed) + Map fieldTypes = new HashMap<>(); // fieldName -> type + + // Pattern to match primitive field declarations + Pattern primitiveFieldPattern = Pattern.compile("private\\s+(int|long)\\s+(\\w+)\\s*;"); + Matcher primitiveFieldMatcher = primitiveFieldPattern.matcher(code); + + while (primitiveFieldMatcher.find()) { + String fieldType = primitiveFieldMatcher.group(1); + String fieldName = primitiveFieldMatcher.group(2); + fieldTypes.put(fieldName, fieldType); + } + + // Pattern to match boxed field declarations + Pattern boxedFieldPattern = Pattern.compile("private\\s+(java\\.lang\\.Integer|java\\.lang\\.Long)\\s+(\\w+)\\s*;"); + Matcher boxedFieldMatcher = boxedFieldPattern.matcher(code); + + while (boxedFieldMatcher.find()) { + String fieldType = boxedFieldMatcher.group(1); + String fieldName = boxedFieldMatcher.group(2); + fieldTypes.put(fieldName, fieldType); + } + + if (fieldTypes.isEmpty()) { + return code; // No numeric fields found + } + + // Find the class name + Pattern classNamePattern = Pattern.compile("public\\s+class\\s+(\\w+)"); + Matcher classNameMatcher = classNamePattern.matcher(code); + if (!classNameMatcher.find()) { + return code; // Can't find class name + } + String className = classNameMatcher.group(1); + + // Find the all-args constructor + Pattern constructorPattern = Pattern.compile( + "public\\s+" + className + "\\s*\\(([^)]*)\\)\\s*\\{([^}]*)\\}" + ); + Matcher constructorMatcher = constructorPattern.matcher(code); + + // Try to find a constructor with parameters + boolean foundConstructor = false; + String constructorParams = ""; + int constructorEnd = -1; + + // Skip the default constructor (no args) if present + if (constructorMatcher.find()) { + String params = constructorMatcher.group(1).trim(); + if (!params.isEmpty()) { + // Found a constructor with parameters + foundConstructor = true; + constructorParams = params; + constructorEnd = constructorMatcher.end(); + } else { + // Found a default constructor, try to find another constructor with parameters + if (constructorMatcher.find()) { + foundConstructor = true; + constructorParams = constructorMatcher.group(1).trim(); + constructorEnd = constructorMatcher.end(); + } + } + } + + // If no constructor with parameters was found, return the original code + if (!foundConstructor || constructorParams.isEmpty()) { + return code; + } + + // Parse the constructor parameters using a more robust approach that handles generic types + List paramTypes = new ArrayList<>(); + List paramNames = new ArrayList<>(); + Map originalParamTypes = new HashMap<>(); // paramName -> original type + + // Parse parameters more carefully to handle generics + parseConstructorParameters(constructorParams, paramTypes, paramNames, originalParamTypes); + + // Generate the overloaded constructor with swapped numeric types + StringBuilder overloadedConstructor = new StringBuilder(); + overloadedConstructor.append("\n /**\n"); + overloadedConstructor.append(" * All-args constructor with flexible numeric type conversion.\n"); + overloadedConstructor.append(" * Allows Integer parameters for Long fields and Long parameters for Integer fields.\n"); + + // Add parameter javadoc + for (int i = 0; i < paramNames.size(); i++) { + overloadedConstructor.append(" * @param ").append(paramNames.get(i)).append(" The new value for ").append(paramNames.get(i)).append("\n"); + } + + overloadedConstructor.append(" */\n"); + overloadedConstructor.append(" public ").append(className).append("("); + + // Generate parameters with swapped types for numeric fields + Map swappedParamTypes = new HashMap<>(); // paramName -> swapped type + + for (int i = 0; i < paramTypes.size(); i++) { + if (i > 0) { + overloadedConstructor.append(", "); + } + + String paramType = paramTypes.get(i); + String paramName = paramNames.get(i); + + // Swap Java types for numeric fields + if (CodeTransformationUtils.JAVA_LANG_LONG.equals(paramType) && fieldTypes.containsKey(paramName)) { + overloadedConstructor.append(CodeTransformationUtils.JAVA_LANG_INTEGER + " ").append(paramName); + swappedParamTypes.put(paramName, CodeTransformationUtils.JAVA_LANG_INTEGER); + } else if (CodeTransformationUtils.JAVA_LANG_INTEGER.equals(paramType) && fieldTypes.containsKey(paramName)) { + overloadedConstructor.append(CodeTransformationUtils.JAVA_LANG_LONG + " ").append(paramName); + swappedParamTypes.put(paramName, CodeTransformationUtils.JAVA_LANG_LONG); + } else { + overloadedConstructor.append(paramType).append(" ").append(paramName); + swappedParamTypes.put(paramName, paramType); + } + } + + overloadedConstructor.append(") {\n"); + + // Generate constructor body with type conversion logic + for (int i = 0; i < paramNames.size(); i++) { + String paramName = paramNames.get(i); + String fieldType = fieldTypes.get(paramName); + String swappedParamType = swappedParamTypes.get(paramName); + + if (fieldType != null) { + if ("int".equals(fieldType) && CodeTransformationUtils.JAVA_LANG_LONG.equals(swappedParamType)) { + // Convert Long to int with bounds check + overloadedConstructor.append(" if (").append(paramName).append(" == null) {\n"); + overloadedConstructor.append(" this.").append(paramName).append(" = 0;\n"); + overloadedConstructor.append(" } else if (").append(paramName).append(" <= Integer.MAX_VALUE && ") + .append(paramName).append(" >= Integer.MIN_VALUE) {\n"); + overloadedConstructor.append(" this.").append(paramName).append(" = ").append(paramName).append(".intValue();\n"); + overloadedConstructor.append(" } else {\n"); + overloadedConstructor.append(" throw new org.apache.avro.AvroRuntimeException(\"Long value \" + ") + .append(paramName).append(" + \" cannot be cast to int\");\n"); + overloadedConstructor.append(" }\n"); + } else if ("long".equals(fieldType) && CodeTransformationUtils.JAVA_LANG_INTEGER.equals(swappedParamType)) { + // Convert Integer to long + overloadedConstructor.append(" this.").append(paramName).append(" = ").append(paramName).append(" == null ? 0L : ") + .append(paramName).append(".longValue();\n"); + } else if (CodeTransformationUtils.JAVA_LANG_INTEGER.equals(fieldType) + && CodeTransformationUtils.JAVA_LANG_LONG.equals(swappedParamType)) { + // Convert Long to Integer with bounds check + overloadedConstructor.append(" if (").append(paramName).append(" == null) {\n"); + overloadedConstructor.append(" this.").append(paramName).append(" = null;\n"); + overloadedConstructor.append(" } else if (").append(paramName).append(" <= Integer.MAX_VALUE && ") + .append(paramName).append(" >= Integer.MIN_VALUE) {\n"); + overloadedConstructor.append(" this.").append(paramName).append(" = ").append(paramName).append(".intValue();\n"); + overloadedConstructor.append(" } else {\n"); + overloadedConstructor.append(" throw new org.apache.avro.AvroRuntimeException(\"Long value \" + ") + .append(paramName).append(" + \" cannot be cast to Integer\");\n"); + overloadedConstructor.append(" }\n"); + } else if (CodeTransformationUtils.JAVA_LANG_LONG.equals(fieldType) + && CodeTransformationUtils.JAVA_LANG_INTEGER.equals(swappedParamType)) { + // Convert Integer to Long + overloadedConstructor.append(" if (").append(paramName).append(" == null) {\n"); + overloadedConstructor.append(" this.").append(paramName).append(" = null;\n"); + overloadedConstructor.append(" } else {\n"); + overloadedConstructor.append(" this.").append(paramName).append(" = ").append(paramName).append(".longValue();\n"); + overloadedConstructor.append(" }\n"); + } else { + // For non-numeric fields or fields with the same type, just assign directly + overloadedConstructor.append(" this.").append(paramName).append(" = ").append(paramName).append(";\n"); + } + } else { + // For fields not in our fieldTypes map, just assign directly + overloadedConstructor.append(" this.").append(paramName).append(" = ").append(paramName).append(";\n"); + } + } + + overloadedConstructor.append(" }"); + + // Insert the overloaded constructor after the original constructor + StringBuilder result = new StringBuilder(code); + result.insert(constructorEnd, "\n" + overloadedConstructor.toString()); + + return result.toString(); + } + + /** + * Parses constructor parameters handling complex generic types correctly. + * This method properly handles nested generics like Map>. + * + * @param constructorParams The constructor parameter string + * @param paramTypes Output list to store parameter types + * @param paramNames Output list to store parameter names + * @param originalParamTypes Output map to store parameter name to type mapping + */ + private static void parseConstructorParameters(String constructorParams, + List paramTypes, + List paramNames, + Map originalParamTypes) { + if (constructorParams == null || constructorParams.trim().isEmpty()) { + return; + } + + int pos = 0; + int len = constructorParams.length(); + int angleNestLevel = 0; + int startPos = 0; + + while (pos < len) { + char c = constructorParams.charAt(pos); + + if (c == '<') { + angleNestLevel++; + } else if (c == '>') { + angleNestLevel--; + } else if (c == ',' && angleNestLevel == 0) { + // Found a top-level comma, which separates parameters + String param = constructorParams.substring(startPos, pos).trim(); + processParameter(param, paramTypes, paramNames, originalParamTypes); + startPos = pos + 1; + } + + pos++; + } + + // Process the last parameter + if (startPos < len) { + String param = constructorParams.substring(startPos).trim(); + processParameter(param, paramTypes, paramNames, originalParamTypes); + } + } + + /** + * Processes a single parameter string and extracts the type and name. + * + * @param param The parameter string (e.g., "java.lang.Integer fieldName") + * @param paramTypes Output list to store parameter types + * @param paramNames Output list to store parameter names + * @param originalParamTypes Output map to store parameter name to type mapping + */ + private static void processParameter(String param, + List paramTypes, + List paramNames, + Map originalParamTypes) { + if (param == null || param.trim().isEmpty()) { + return; + } + + // Find the last space which separates the type from the name + int lastSpacePos = param.lastIndexOf(' '); + if (lastSpacePos > 0) { + String paramType = param.substring(0, lastSpacePos).trim(); + String paramName = param.substring(lastSpacePos + 1).trim(); + + paramTypes.add(paramType); + paramNames.add(paramName); + originalParamTypes.put(paramName, paramType); + } + } + + + /** + * Adds overloaded setter methods for numeric fields in the builder class to allow setting Integer fields with Long values + * and Long fields with Integer values. This improves type compatibility when working with numeric fields in the builder pattern. + * This method preserves existing builder setter methods and only adds the overloaded versions. + * + * @param code generated code + * @return transformed code with overloaded numeric builder setter methods + */ + public static String addOverloadedNumericBuilderSetterMethods(String code) { + if (code == null || code.isEmpty()) { + return code; + } + + // First, identify the builder class in the code + Pattern builderClassPattern = Pattern.compile("public static class Builder[^{]*\\{"); + Matcher builderClassMatcher = builderClassPattern.matcher(code); + + if (!builderClassMatcher.find()) { + return code; // No builder class found + } + + int builderClassStart = builderClassMatcher.start(); + + // Find the opening brace of the builder class + int builderClassOpenBrace = code.indexOf("{", builderClassStart); + if (builderClassOpenBrace == -1) { + return code; // Unexpected format + } + + // Find the end of the builder class using the helper method + int builderClassEnd = CodeTransformationUtils.findEndOfBlock(code, builderClassOpenBrace); + if (builderClassEnd == -1) { + return code; // Couldn't find proper end of builder class + } + + String builderClassCode = code.substring(builderClassStart, builderClassEnd); + + // Extract the class name to determine the builder return type + Pattern classNamePattern = Pattern.compile("public class (\\w+)"); + Matcher classNameMatcher = classNamePattern.matcher(code); + if (!classNameMatcher.find()) { + return code; // Can't find class name + } + String className = classNameMatcher.group(1); + + // Extract namespace/package name to correctly reference the builder class + String namespace = ""; + Pattern packagePattern = Pattern.compile("package\\s+([^;]+);"); + Matcher packageMatcher = packagePattern.matcher(code); + if (packageMatcher.find()) { + namespace = packageMatcher.group(1) + "."; + } + + String builderReturnType = namespace + className + ".Builder"; + + // Find numeric setter methods in the builder class + Pattern builderSetterPattern = Pattern.compile( + "public\\s+[\\w.]+\\.Builder\\s+(\\w+)\\s*\\(\\s*(int|long|java\\.lang\\.Integer|java\\.lang\\.Long)\\s+(\\w+)\\s*\\)\\s*\\{"); + Matcher builderSetterMatcher = builderSetterPattern.matcher(builderClassCode); + + StringBuilder modifiedBuilderClass = new StringBuilder(builderClassCode); + int offset = 0; + + while (builderSetterMatcher.find()) { + String methodName = builderSetterMatcher.group(1); + String paramType = builderSetterMatcher.group(2); + String fieldName = builderSetterMatcher.group(3); + + // Only process methods that start with "set" + if (!methodName.startsWith("set")) { + continue; + } + + // Find the position of the method body start + int methodStart = builderSetterMatcher.start() + offset; + int methodBodyStart = modifiedBuilderClass.indexOf("{", methodStart); + if (methodBodyStart == -1) continue; + + // Find the end of the method using the helper + int methodRelativeEnd = CodeTransformationUtils.findEndOfBlock(modifiedBuilderClass.toString(), methodBodyStart); + if (methodRelativeEnd == -1) continue; // Couldn't find proper end of method + + int methodEnd = methodRelativeEnd; + + // Generate the overloaded setter using the utility method + StringBuilder overloadedSetter = CodeTransformationUtils.generateOverloadedSetter( + methodName, fieldName, paramType, true, builderReturnType); + + if (overloadedSetter.length() == 0) { + continue; // Not a numeric type we handle + } + + // Check if the overloaded method already exists in the builder class + String overloadSignature = null; + if ("int".equals(paramType)) { + overloadSignature = "public " + builderReturnType + " " + methodName + "(long "; + } else if ("long".equals(paramType)) { + overloadSignature = "public " + builderReturnType + " " + methodName + "(int "; + } else if (CodeTransformationUtils.JAVA_LANG_INTEGER.equals(paramType)) { + overloadSignature = "public " + builderReturnType + " " + methodName + "(java.lang.Long "; + } else if (CodeTransformationUtils.JAVA_LANG_LONG.equals(paramType)) { + overloadSignature = "public " + builderReturnType + " " + methodName + "(java.lang.Integer "; + } + + // Only add if it doesn't already exist + if (overloadSignature != null && !modifiedBuilderClass.toString().contains(overloadSignature)) { + // Insert the overloaded setter after the existing setter + modifiedBuilderClass.insert(methodEnd, overloadedSetter); + offset += overloadedSetter.length(); + } + } + + // Replace the builder class in the original code + return code.substring(0, builderClassStart) + modifiedBuilderClass + code.substring(builderClassEnd); + } + private static String addImports(String code, Collection importStatements) { if (importStatements == null || importStatements.isEmpty()) { return code; diff --git a/helper/helper-common/src/test/java/com/linkedin/avroutil1/compatibility/CodeTransformationUtilsTest.java b/helper/helper-common/src/test/java/com/linkedin/avroutil1/compatibility/CodeTransformationUtilsTest.java new file mode 100644 index 000000000..de9fffa13 --- /dev/null +++ b/helper/helper-common/src/test/java/com/linkedin/avroutil1/compatibility/CodeTransformationUtilsTest.java @@ -0,0 +1,167 @@ +/* + * Copyright 2020 LinkedIn Corp. + * Licensed under the BSD 2-Clause License (the "License"). + * See License in the project root for license information. + */ + +package com.linkedin.avroutil1.compatibility; + +import org.testng.Assert; +import org.testng.annotations.Test; + + +public class CodeTransformationUtilsTest { + @Test + public void testFindEndOfBlock() { + // Test basic code block + String code1 = "public void method() { int x = 1; return; }"; + int result1 = CodeTransformationUtils.findEndOfBlock(code1, code1.indexOf('{')); + Assert.assertEquals(result1, code1.length()); + + // Test nested blocks + String code2 = "public void method() { if (true) { int x = 1; } return; }"; + int result2 = CodeTransformationUtils.findEndOfBlock(code2, code2.indexOf('{')); + Assert.assertEquals(result2, code2.length()); + + // Test multiple nested blocks + String code3 = "public void method() { if (true) { while(x < 10) { x++; } } return; }"; + int result3 = CodeTransformationUtils.findEndOfBlock(code3, code3.indexOf('{')); + Assert.assertEquals(result3, code3.length()); + + // Test invalid start position + int result4 = CodeTransformationUtils.findEndOfBlock("public void method() {}", -1); + Assert.assertEquals(result4, -1); + + // Test no opening brace + int result5 = CodeTransformationUtils.findEndOfBlock("public void method()", 0); + Assert.assertEquals(result5, -1); + + // Test unclosed block + String code6 = "public void method() { if (true) {"; + int result6 = CodeTransformationUtils.findEndOfBlock(code6, code6.indexOf('{')); + Assert.assertEquals(result6, -1); + + // Test second block in code + String code7 = "public void method1() {} public void method2() { return; }"; + int firstBraceIndex = code7.indexOf('{'); + int secondBraceIndex = code7.indexOf('{', firstBraceIndex + 1); + int result7 = CodeTransformationUtils.findEndOfBlock(code7, secondBraceIndex); + Assert.assertEquals(result7, code7.length()); + } + + @Test + public void testGenerateNumericConversionJavadoc() { + // Test Integer to Long conversion (regular setter) + StringBuilder result1 = CodeTransformationUtils.generateNumericConversionJavadoc( + "count", "java.lang.Integer", "java.lang.Long", false); + Assert.assertTrue(result1.toString().contains("Sets count to the specified value")); + Assert.assertTrue(result1.toString().contains("Accepts an Integer value and converts it to Long")); + Assert.assertTrue(result1.toString().contains("@param value The Integer value to set")); + Assert.assertFalse(result1.toString().contains("@return")); + + // Test long to int conversion (builder method) + StringBuilder result2 = CodeTransformationUtils.generateNumericConversionJavadoc( + "age", "long", "int", true); + Assert.assertTrue(result2.toString().contains("Sets age to the specified value")); + Assert.assertTrue(result2.toString().contains("Accepts a long value and converts it to int with bounds checking")); + Assert.assertTrue(result2.toString().contains("@param value The long value to set")); + Assert.assertTrue(result2.toString().contains("@return This builder")); + Assert.assertTrue(result2.toString().contains("@throws org.apache.avro.AvroRuntimeException")); + } + + @Test + public void testGenerateNumericMethodSignature() { + // Test regular setter signature + StringBuilder sig1 = CodeTransformationUtils.generateNumericMethodSignature( + "setValueField", "long", "value", "void"); + Assert.assertEquals(sig1.toString(), " public void setValueField(long value) {\n"); + + // Test builder method signature + StringBuilder sig2 = CodeTransformationUtils.generateNumericMethodSignature( + "setValueField", "java.lang.Integer", "value", "Builder"); + Assert.assertEquals(sig2.toString(), " public Builder setValueField(java.lang.Integer value) {\n"); + } + + @Test + public void testGenerateNumericConversionCode() { + // Test long to int conversion (regular setter) + StringBuilder code1 = CodeTransformationUtils.generateNumericConversionCode("long", "int", false, "setSizeField", + null); + Assert.assertTrue(code1.toString().contains("if (value <= Integer.MAX_VALUE && value >= Integer.MIN_VALUE)")); + Assert.assertTrue(code1.toString().contains("setSizeField((int) value)")); + Assert.assertTrue(code1.toString().contains("throw new org.apache.avro.AvroRuntimeException")); + + // Test int to long conversion (builder method) + StringBuilder code2 = CodeTransformationUtils.generateNumericConversionCode("int", "long", true, null, "setSizeField"); + Assert.assertTrue(code2.toString().contains("return setSizeField((long) value)")); + + // Test Long to Integer conversion (regular setter with null handling) + StringBuilder code3 = CodeTransformationUtils.generateNumericConversionCode("java.lang.Long", "java.lang.Integer", false, + "setSizeField", null); + Assert.assertTrue(code3.toString().contains("if (value == null)")); + Assert.assertTrue(code3.toString().contains("setSizeField((java.lang.Integer) null)")); + Assert.assertTrue(code3.toString().contains("setSizeField(value.intValue())")); + + // Test Integer to Long conversion (builder method with null handling) + StringBuilder code4 = CodeTransformationUtils.generateNumericConversionCode("java.lang.Integer", "java.lang.Long", true, + null, "setSizeField"); + Assert.assertTrue(code4.toString().contains("if (value == null)")); + Assert.assertTrue(code4.toString().contains("return setSizeField((java.lang.Long) null)")); + Assert.assertTrue(code4.toString().contains("return setSizeField(value.longValue())")); + } + + @Test + public void testDetermineOverloadSignature() { + // Test primitive types + Assert.assertEquals(CodeTransformationUtils.determineOverloadSignature("int", "setValue"), + "public void setValue(long "); + Assert.assertEquals(CodeTransformationUtils.determineOverloadSignature("long", "setValue"), + "public void setValue(int "); + + // Test wrapper types + Assert.assertEquals(CodeTransformationUtils.determineOverloadSignature("java.lang.Integer", "setValue"), + "public void setValue(java.lang.Long "); + Assert.assertEquals(CodeTransformationUtils.determineOverloadSignature("java.lang.Long", "setValue"), + "public void setValue(java.lang.Integer "); + + // Test unsupported type + Assert.assertNull(CodeTransformationUtils.determineOverloadSignature("String", "setValue")); + } + + @Test + public void testGenerateOverloadedSetter() { + // Test primitive int field with regular setter + StringBuilder setter1 = CodeTransformationUtils.generateOverloadedSetter( + "setCount", "count", "int", false, null); + Assert.assertTrue(setter1.toString().contains("public void setCount(long value)")); + Assert.assertTrue(setter1.toString().contains("if (value <= Integer.MAX_VALUE && value >= Integer.MIN_VALUE)")); + Assert.assertTrue(setter1.toString().contains("setCount((int) value)")); + + // Test primitive long field with builder method + StringBuilder setter2 = CodeTransformationUtils.generateOverloadedSetter( + "withSize", "size", "long", true, "Builder"); + Assert.assertTrue(setter2.toString().contains("public Builder withSize(int value)")); + Assert.assertTrue(setter2.toString().contains("return withSize((long) value)")); + + // Test wrapper Integer field with regular setter + StringBuilder setter3 = CodeTransformationUtils.generateOverloadedSetter( + "setCount", "count", "java.lang.Integer", false, null); + Assert.assertTrue(setter3.toString().contains("public void setCount(java.lang.Long value)")); + Assert.assertTrue(setter3.toString().contains("if (value == null)")); + Assert.assertTrue(setter3.toString().contains("setCount((java.lang.Integer) null)")); + Assert.assertTrue(setter3.toString().contains("setCount(value.intValue())")); + + // Test wrapper Long field with builder method + StringBuilder setter4 = CodeTransformationUtils.generateOverloadedSetter( + "withSize", "size", "java.lang.Long", true, "Builder"); + Assert.assertTrue(setter4.toString().contains("public Builder withSize(java.lang.Integer value)")); + Assert.assertTrue(setter4.toString().contains("if (value == null)")); + Assert.assertTrue(setter4.toString().contains("return withSize((java.lang.Long) null)")); + Assert.assertTrue(setter4.toString().contains("return withSize(value.longValue())")); + + // Test unsupported type (should return empty StringBuilder) + StringBuilder setter5 = CodeTransformationUtils.generateOverloadedSetter( + "setName", "name", "String", false, null); + Assert.assertEquals(setter5.length(), 0); + } +} diff --git a/helper/tests/codegen-110/src/main/compat-avro-w-builders/under110wbuilders/IntsAndLongsWithBuilder.avsc b/helper/tests/codegen-110/src/main/compat-avro-w-builders/under110wbuilders/IntsAndLongsWithBuilder.avsc new file mode 100644 index 000000000..fa0395191 --- /dev/null +++ b/helper/tests/codegen-110/src/main/compat-avro-w-builders/under110wbuilders/IntsAndLongsWithBuilder.avsc @@ -0,0 +1,25 @@ +{ + "type": "record", + "namespace": "under110wbuilders", + "name": "IntsAndLongsWithBuilder", + "fields": [ + { + "name": "longField", + "type": "long" + }, + { + "name": "intField", + "type": "int" + }, + { + "name": "boxedLongField", + "type": ["null", "long"], + "default": null + }, + { + "name": "boxedIntField", + "type": ["null", "int"], + "default": null + } + ] +} diff --git a/helper/tests/codegen-110/src/main/compat-avro/under110/IntsAndLongs.avsc b/helper/tests/codegen-110/src/main/compat-avro/under110/IntsAndLongs.avsc new file mode 100644 index 000000000..50fa51cce --- /dev/null +++ b/helper/tests/codegen-110/src/main/compat-avro/under110/IntsAndLongs.avsc @@ -0,0 +1,25 @@ +{ + "type": "record", + "namespace": "under110", + "name": "IntsAndLongs", + "fields": [ + { + "name": "longField", + "type": "long" + }, + { + "name": "intField", + "type": "int" + }, + { + "name": "boxedLongField", + "type": ["null", "long"], + "default": null + }, + { + "name": "boxedIntField", + "type": ["null", "int"], + "default": null + } + ] +} \ No newline at end of file diff --git a/helper/tests/codegen-111/src/main/compat-avro-w-builders/under111wbuilders/IntsAndLongsWithBuilder.avsc b/helper/tests/codegen-111/src/main/compat-avro-w-builders/under111wbuilders/IntsAndLongsWithBuilder.avsc new file mode 100644 index 000000000..29a7c3c5c --- /dev/null +++ b/helper/tests/codegen-111/src/main/compat-avro-w-builders/under111wbuilders/IntsAndLongsWithBuilder.avsc @@ -0,0 +1,25 @@ +{ + "type": "record", + "namespace": "under111wbuilders", + "name": "IntsAndLongsWithBuilder", + "fields": [ + { + "name": "longField", + "type": "long" + }, + { + "name": "intField", + "type": "int" + }, + { + "name": "boxedLongField", + "type": ["null", "long"], + "default": null + }, + { + "name": "boxedIntField", + "type": ["null", "int"], + "default": null + } + ] +} diff --git a/helper/tests/codegen-111/src/main/compat-avro/under111/IntsAndLongs.avsc b/helper/tests/codegen-111/src/main/compat-avro/under111/IntsAndLongs.avsc new file mode 100644 index 000000000..22ed16555 --- /dev/null +++ b/helper/tests/codegen-111/src/main/compat-avro/under111/IntsAndLongs.avsc @@ -0,0 +1,25 @@ +{ + "type": "record", + "namespace": "under111", + "name": "IntsAndLongs", + "fields": [ + { + "name": "longField", + "type": "long" + }, + { + "name": "intField", + "type": "int" + }, + { + "name": "boxedLongField", + "type": ["null", "long"], + "default": null + }, + { + "name": "boxedIntField", + "type": ["null", "int"], + "default": null + } + ] +} diff --git a/helper/tests/codegen-14/src/main/compat-avro/under14/IntsAndLongs.avsc b/helper/tests/codegen-14/src/main/compat-avro/under14/IntsAndLongs.avsc new file mode 100644 index 000000000..da42ec88c --- /dev/null +++ b/helper/tests/codegen-14/src/main/compat-avro/under14/IntsAndLongs.avsc @@ -0,0 +1,25 @@ +{ + "type": "record", + "namespace": "under14", + "name": "IntsAndLongs", + "fields": [ + { + "name": "longField", + "type": "long" + }, + { + "name": "intField", + "type": "int" + }, + { + "name": "boxedLongField", + "type": ["null", "long"], + "default": null + }, + { + "name": "boxedIntField", + "type": ["null", "int"], + "default": null + } + ] +} diff --git a/helper/tests/codegen-15/src/main/compat-avro/under15/IntsAndLongs.avsc b/helper/tests/codegen-15/src/main/compat-avro/under15/IntsAndLongs.avsc new file mode 100644 index 000000000..7101398f7 --- /dev/null +++ b/helper/tests/codegen-15/src/main/compat-avro/under15/IntsAndLongs.avsc @@ -0,0 +1,25 @@ +{ + "type": "record", + "namespace": "under15", + "name": "IntsAndLongs", + "fields": [ + { + "name": "longField", + "type": "long" + }, + { + "name": "intField", + "type": "int" + }, + { + "name": "boxedLongField", + "type": ["null", "long"], + "default": null + }, + { + "name": "boxedIntField", + "type": ["null", "int"], + "default": null + } + ] +} diff --git a/helper/tests/codegen-16/src/main/compat-avro/under16/IntsAndLongs.avsc b/helper/tests/codegen-16/src/main/compat-avro/under16/IntsAndLongs.avsc new file mode 100644 index 000000000..4c5243b68 --- /dev/null +++ b/helper/tests/codegen-16/src/main/compat-avro/under16/IntsAndLongs.avsc @@ -0,0 +1,25 @@ +{ + "type": "record", + "namespace": "under16", + "name": "IntsAndLongs", + "fields": [ + { + "name": "longField", + "type": "long" + }, + { + "name": "intField", + "type": "int" + }, + { + "name": "boxedLongField", + "type": ["null", "long"], + "default": null + }, + { + "name": "boxedIntField", + "type": ["null", "int"], + "default": null + } + ] +} diff --git a/helper/tests/codegen-17/src/main/compat-avro/under17/IntsAndLongs.avsc b/helper/tests/codegen-17/src/main/compat-avro/under17/IntsAndLongs.avsc new file mode 100644 index 000000000..3782dd70d --- /dev/null +++ b/helper/tests/codegen-17/src/main/compat-avro/under17/IntsAndLongs.avsc @@ -0,0 +1,25 @@ +{ + "type": "record", + "namespace": "under17", + "name": "IntsAndLongs", + "fields": [ + { + "name": "longField", + "type": "long" + }, + { + "name": "intField", + "type": "int" + }, + { + "name": "boxedLongField", + "type": ["null", "long"], + "default": null + }, + { + "name": "boxedIntField", + "type": ["null", "int"], + "default": null + } + ] +} diff --git a/helper/tests/codegen-18/src/main/compat-avro-w-builders/under18wbuilders/IntsAndLongsWithBuilder.avsc b/helper/tests/codegen-18/src/main/compat-avro-w-builders/under18wbuilders/IntsAndLongsWithBuilder.avsc new file mode 100644 index 000000000..95e4b4738 --- /dev/null +++ b/helper/tests/codegen-18/src/main/compat-avro-w-builders/under18wbuilders/IntsAndLongsWithBuilder.avsc @@ -0,0 +1,25 @@ +{ + "type": "record", + "namespace": "under18wbuilders", + "name": "IntsAndLongsWithBuilder", + "fields": [ + { + "name": "longField", + "type": "long" + }, + { + "name": "intField", + "type": "int" + }, + { + "name": "boxedLongField", + "type": ["null", "long"], + "default": null + }, + { + "name": "boxedIntField", + "type": ["null", "int"], + "default": null + } + ] +} diff --git a/helper/tests/codegen-18/src/main/compat-avro/under18/IntsAndLongs.avsc b/helper/tests/codegen-18/src/main/compat-avro/under18/IntsAndLongs.avsc new file mode 100644 index 000000000..c4b301b1c --- /dev/null +++ b/helper/tests/codegen-18/src/main/compat-avro/under18/IntsAndLongs.avsc @@ -0,0 +1,25 @@ +{ + "type": "record", + "namespace": "under18", + "name": "IntsAndLongs", + "fields": [ + { + "name": "longField", + "type": "long" + }, + { + "name": "intField", + "type": "int" + }, + { + "name": "boxedLongField", + "type": ["null", "long"], + "default": null + }, + { + "name": "boxedIntField", + "type": ["null", "int"], + "default": null + } + ] +} diff --git a/helper/tests/codegen-19/src/main/compat-avro-w-builders/under19wbuilders/IntsAndLongsWithBuilder.avsc b/helper/tests/codegen-19/src/main/compat-avro-w-builders/under19wbuilders/IntsAndLongsWithBuilder.avsc new file mode 100644 index 000000000..09fb0fbec --- /dev/null +++ b/helper/tests/codegen-19/src/main/compat-avro-w-builders/under19wbuilders/IntsAndLongsWithBuilder.avsc @@ -0,0 +1,25 @@ +{ + "type": "record", + "namespace": "under19wbuilders", + "name": "IntsAndLongsWithBuilder", + "fields": [ + { + "name": "longField", + "type": "long" + }, + { + "name": "intField", + "type": "int" + }, + { + "name": "boxedLongField", + "type": ["null", "long"], + "default": null + }, + { + "name": "boxedIntField", + "type": ["null", "int"], + "default": null + } + ] +} diff --git a/helper/tests/codegen-19/src/main/compat-avro/under19/IntsAndLongs.avsc b/helper/tests/codegen-19/src/main/compat-avro/under19/IntsAndLongs.avsc new file mode 100644 index 000000000..68b6be6ec --- /dev/null +++ b/helper/tests/codegen-19/src/main/compat-avro/under19/IntsAndLongs.avsc @@ -0,0 +1,25 @@ +{ + "type": "record", + "namespace": "under19", + "name": "IntsAndLongs", + "fields": [ + { + "name": "longField", + "type": "long" + }, + { + "name": "intField", + "type": "int" + }, + { + "name": "boxedLongField", + "type": ["null", "long"], + "default": null + }, + { + "name": "boxedIntField", + "type": ["null", "int"], + "default": null + } + ] +} diff --git a/helper/tests/helper-tests-110/src/test/java/com/linkedin/avroutil1/compatibility/avro110/CodeTransformationsAvro110Test.java b/helper/tests/helper-tests-110/src/test/java/com/linkedin/avroutil1/compatibility/avro110/CodeTransformationsAvro110Test.java index 24268670d..d3eabc8f9 100644 --- a/helper/tests/helper-tests-110/src/test/java/com/linkedin/avroutil1/compatibility/avro110/CodeTransformationsAvro110Test.java +++ b/helper/tests/helper-tests-110/src/test/java/com/linkedin/avroutil1/compatibility/avro110/CodeTransformationsAvro110Test.java @@ -6,10 +6,16 @@ package com.linkedin.avroutil1.compatibility.avro110; -import com.linkedin.avroutil1.testcommon.TestUtil; import com.linkedin.avroutil1.compatibility.AvroCompatibilityHelper; import com.linkedin.avroutil1.compatibility.AvroVersion; import com.linkedin.avroutil1.compatibility.CodeTransformations; +import com.linkedin.avroutil1.testcommon.TestUtil; +import java.io.File; +import java.io.FileInputStream; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.regex.Matcher; import net.openhft.compiler.CompilerUtils; import org.apache.avro.Schema; import org.apache.avro.compiler.specific.SpecificCompiler; @@ -17,14 +23,6 @@ import org.apache.commons.io.IOUtils; import org.testng.Assert; import org.testng.annotations.Test; - -import java.io.File; -import java.io.FileInputStream; -import java.lang.reflect.Method; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.util.regex.Matcher; - import under110wbuildersmin18.NormalRecordWithoutReferences; @@ -158,6 +156,80 @@ public void testBuilders() throws Exception { Assert.assertNotNull(NormalRecordWithoutReferences.newBuilder()); } + @Test + public void testEnhanceNumericPutMethod() throws Exception { + // Use the IntsAndLongs schema for testing + String avsc = TestUtil.load("IntsAndLongs.avsc"); + Schema schema = AvroCompatibilityHelper.parse(avsc); + String originalCode = runNativeCodegen(schema); + + // Apply the transformation + String enhancedCode = CodeTransformations.enhanceNumericPutMethod(originalCode); + + // Verify the enhanced code contains the type checking for numeric conversions + Assert.assertTrue(enhancedCode.contains("if (value$ instanceof java.lang.Long)")); + Assert.assertTrue(enhancedCode.contains("if (value$ instanceof java.lang.Integer)")); + + // Compile the enhanced code to verify it's valid Java + try { + CompilerUtils.CACHED_COMPILER.loadFromJava(schema.getFullName(), enhancedCode); + } catch (Exception e) { + Assert.fail("Enhanced put method code should compile without errors"); + } + } + + @Test + public void testAddOverloadedNumericSetterMethods() throws Exception { + // Use the IntsAndLongs schema for testing + String avsc = TestUtil.load("IntsAndLongs.avsc"); + Schema schema = AvroCompatibilityHelper.parse(avsc); + String originalCode = runNativeCodegen(schema); + + // Apply the transformation + String enhancedCode = CodeTransformations.addOverloadedNumericSetterMethods(originalCode); + + // Verify the enhanced code contains the overloaded setters + Assert.assertTrue(enhancedCode.contains("public void setIntField(long value)")); + Assert.assertTrue(enhancedCode.contains("public void setLongField(int value)")); + Assert.assertTrue(enhancedCode.contains("public void setBoxedIntField(java.lang.Long value)")); + Assert.assertTrue(enhancedCode.contains("public void setBoxedLongField(java.lang.Integer value)")); + + // Compile the enhanced code to verify it's valid Java + try { + CompilerUtils.CACHED_COMPILER.loadFromJava(schema.getFullName(), enhancedCode); + } catch (Exception e) { + Assert.fail("Enhanced setter methods code should compile without errors"); + } + } + + @Test + public void testAddOverloadedNumericConstructor() throws Exception { + // Use the IntsAndLongs schema for testing + String avsc = TestUtil.load("IntsAndLongs.avsc"); + Schema schema = AvroCompatibilityHelper.parse(avsc); + String originalCode = runNativeCodegen(schema); + + // Apply the transformation + String enhancedCode = CodeTransformations.addOverloadedNumericConstructor(originalCode); + + // Verify the enhanced code contains the overloaded constructor with swapped types + Assert.assertTrue(enhancedCode.contains("public " + schema.getName() + "(java.lang.Integer longField, java.lang.Long " + + "intField, java.lang.Integer boxedLongField, java.lang.Long boxedIntField)")); + + // Compile the enhanced code to verify it's valid Java + Class generatedClass = null; + try { + generatedClass = CompilerUtils.CACHED_COMPILER.loadFromJava(schema.getFullName(), enhancedCode); + } catch (Exception e) { + Assert.fail("Enhanced constructor code should compile without errors"); + } + + Assert.assertNotNull(generatedClass.getConstructor( + java.lang.Long.class, java.lang.Integer.class, java.lang.Long.class, java.lang.Integer.class)); + Assert.assertNotNull(generatedClass.getConstructor( + java.lang.Integer.class, java.lang.Long.class, java.lang.Integer.class, java.lang.Long.class)); + } + private String runNativeCodegen(Schema schema) throws Exception { File outputRoot = Files.createTempDirectory(null).toFile(); SpecificCompiler compiler = new SpecificCompiler(schema); diff --git a/helper/tests/helper-tests-111/src/test/java/com/linkedin/avroutil1/compatibility/avro111/CodeTransformationsAvro111Test.java b/helper/tests/helper-tests-111/src/test/java/com/linkedin/avroutil1/compatibility/avro111/CodeTransformationsAvro111Test.java index e03f194f8..fa0ec610b 100644 --- a/helper/tests/helper-tests-111/src/test/java/com/linkedin/avroutil1/compatibility/avro111/CodeTransformationsAvro111Test.java +++ b/helper/tests/helper-tests-111/src/test/java/com/linkedin/avroutil1/compatibility/avro111/CodeTransformationsAvro111Test.java @@ -10,6 +10,12 @@ import com.linkedin.avroutil1.compatibility.AvroVersion; import com.linkedin.avroutil1.compatibility.CodeTransformations; import com.linkedin.avroutil1.testcommon.TestUtil; +import java.io.File; +import java.io.FileInputStream; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.regex.Matcher; import net.openhft.compiler.CompilerUtils; import org.apache.avro.Schema; import org.apache.avro.compiler.specific.SpecificCompiler; @@ -17,14 +23,6 @@ import org.apache.commons.io.IOUtils; import org.testng.Assert; import org.testng.annotations.Test; - -import java.io.File; -import java.io.FileInputStream; -import java.lang.reflect.Method; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.util.regex.Matcher; - import under111wbuildersmin18.NormalRecordWithoutReferences; @@ -84,6 +82,79 @@ public void testStripCustomCoders() throws Exception { public void testBuilders() throws Exception { Assert.assertNotNull(NormalRecordWithoutReferences.newBuilder()); } + @Test + public void testEnhanceNumericPutMethod() throws Exception { + // Use the IntsAndLongs schema for testing + String avsc = TestUtil.load("IntsAndLongs.avsc"); + Schema schema = AvroCompatibilityHelper.parse(avsc); + String originalCode = runNativeCodegen(schema); + + // Apply the transformation + String enhancedCode = CodeTransformations.enhanceNumericPutMethod(originalCode); + + // Verify the enhanced code contains the type checking for numeric conversions + Assert.assertTrue(enhancedCode.contains("if (value$ instanceof java.lang.Long)")); + Assert.assertTrue(enhancedCode.contains("if (value$ instanceof java.lang.Integer)")); + + // Compile the enhanced code to verify it's valid Java + try { + CompilerUtils.CACHED_COMPILER.loadFromJava(schema.getFullName(), enhancedCode); + } catch (Exception e) { + Assert.fail("Enhanced put method code should compile without errors"); + } + } + + @Test + public void testAddOverloadedNumericSetterMethods() throws Exception { + // Use the IntsAndLongs schema for testing + String avsc = TestUtil.load("IntsAndLongs.avsc"); + Schema schema = AvroCompatibilityHelper.parse(avsc); + String originalCode = runNativeCodegen(schema); + + // Apply the transformation + String enhancedCode = CodeTransformations.addOverloadedNumericSetterMethods(originalCode); + + // Verify the enhanced code contains the overloaded setters + Assert.assertTrue(enhancedCode.contains("public void setIntField(long value)")); + Assert.assertTrue(enhancedCode.contains("public void setLongField(int value)")); + Assert.assertTrue(enhancedCode.contains("public void setBoxedIntField(java.lang.Long value)")); + Assert.assertTrue(enhancedCode.contains("public void setBoxedLongField(java.lang.Integer value)")); + + // Compile the enhanced code to verify it's valid Java + try { + CompilerUtils.CACHED_COMPILER.loadFromJava(schema.getFullName(), enhancedCode); + } catch (Exception e) { + Assert.fail("Enhanced setter methods code should compile without errors"); + } + } + + @Test + public void testAddOverloadedNumericConstructor() throws Exception { + // Use the IntsAndLongs schema for testing + String avsc = TestUtil.load("IntsAndLongs.avsc"); + Schema schema = AvroCompatibilityHelper.parse(avsc); + String originalCode = runNativeCodegen(schema); + + // Apply the transformation + String enhancedCode = CodeTransformations.addOverloadedNumericConstructor(originalCode); + + // Verify the enhanced code contains the overloaded constructor with swapped types + Assert.assertTrue(enhancedCode.contains("public " + schema.getName() + "(java.lang.Integer longField, java.lang.Long " + + "intField, java.lang.Integer boxedLongField, java.lang.Long boxedIntField)")); + + // Compile the enhanced code to verify it's valid Java + Class generatedClass = null; + try { + generatedClass = CompilerUtils.CACHED_COMPILER.loadFromJava(schema.getFullName(), enhancedCode); + } catch (Exception e) { + Assert.fail("Enhanced constructor code should compile without errors"); + } + + Assert.assertNotNull(generatedClass.getConstructor( + java.lang.Long.class, java.lang.Integer.class, java.lang.Long.class, java.lang.Integer.class)); + Assert.assertNotNull(generatedClass.getConstructor( + java.lang.Integer.class, java.lang.Long.class, java.lang.Integer.class, java.lang.Long.class)); + } private String runNativeCodegen(Schema schema) throws Exception { File outputRoot = Files.createTempDirectory(null).toFile(); diff --git a/helper/tests/helper-tests-14/src/test/java/com/linkedin/avroutil1/compatibility/avro14/CodeTransformationsAvro14Test.java b/helper/tests/helper-tests-14/src/test/java/com/linkedin/avroutil1/compatibility/avro14/CodeTransformationsAvro14Test.java index f0c6fd3c2..e0aa7d1f5 100644 --- a/helper/tests/helper-tests-14/src/test/java/com/linkedin/avroutil1/compatibility/avro14/CodeTransformationsAvro14Test.java +++ b/helper/tests/helper-tests-14/src/test/java/com/linkedin/avroutil1/compatibility/avro14/CodeTransformationsAvro14Test.java @@ -148,6 +148,28 @@ public void testAlternateAvscUnderAvro14WithEscaping() throws Exception { Assert.assertEquals(inCode, schema); //no (significant) harm done } + @Test + public void testEnhanceNumericPutMethod() throws Exception { + // Use the IntsAndLongs schema for testing + String avsc = TestUtil.load("IntsAndLongs.avsc"); + Schema schema = AvroCompatibilityHelper.parse(avsc); + String originalCode = runNativeCodegen(schema); + + // Apply the transformation + String enhancedCode = CodeTransformations.enhanceNumericPutMethod(originalCode); + + // Verify the enhanced code contains the type checking for numeric conversions + Assert.assertTrue(enhancedCode.contains("if (value$ instanceof java.lang.Long)")); + Assert.assertTrue(enhancedCode.contains("if (value$ instanceof java.lang.Integer)")); + + // Compile the enhanced code to verify it's valid Java + try { + CompilerUtils.CACHED_COMPILER.loadFromJava(schema.getFullName(), enhancedCode); + } catch (Exception e) { + Assert.fail("Enhanced put method code should compile without errors"); + } + } + private String runNativeCodegen(Schema schema) throws Exception { File outputRoot = Files.createTempDirectory(null).toFile(); return runNativeCodegen(schema, outputRoot); diff --git a/helper/tests/helper-tests-15/src/test/java/com/linkedin/avroutil1/compatibility/avro15/CodeTransformationsAvro15Test.java b/helper/tests/helper-tests-15/src/test/java/com/linkedin/avroutil1/compatibility/avro15/CodeTransformationsAvro15Test.java index 5f97a0e28..bf3e13b74 100644 --- a/helper/tests/helper-tests-15/src/test/java/com/linkedin/avroutil1/compatibility/avro15/CodeTransformationsAvro15Test.java +++ b/helper/tests/helper-tests-15/src/test/java/com/linkedin/avroutil1/compatibility/avro15/CodeTransformationsAvro15Test.java @@ -64,6 +64,28 @@ public void testTransformAvro15RecordWithMultilineDoc() throws Exception { Assert.assertNotNull(transformedClass); } + @Test + public void testEnhanceNumericPutMethod() throws Exception { + // Use the IntsAndLongs schema for testing + String avsc = TestUtil.load("IntsAndLongs.avsc"); + Schema schema = AvroCompatibilityHelper.parse(avsc); + String originalCode = runNativeCodegen(schema); + + // Apply the transformation + String enhancedCode = CodeTransformations.enhanceNumericPutMethod(originalCode); + + // Verify the enhanced code contains the type checking for numeric conversions + Assert.assertTrue(enhancedCode.contains("if (value$ instanceof java.lang.Long)")); + Assert.assertTrue(enhancedCode.contains("if (value$ instanceof java.lang.Integer)")); + + // Compile the enhanced code to verify it's valid Java + try { + CompilerUtils.CACHED_COMPILER.loadFromJava(schema.getFullName(), enhancedCode); + } catch (Exception e) { + Assert.fail("Enhanced put method code should compile without errors"); + } + } + private String runNativeCodegen(Schema schema) throws Exception { File outputRoot = Files.createTempDirectory(null).toFile(); SpecificCompiler compiler = new SpecificCompiler(schema); diff --git a/helper/tests/helper-tests-16/src/test/java/com/linkedin/avroutil1/compatibility/avro16/CodeTransformationsAvro16Test.java b/helper/tests/helper-tests-16/src/test/java/com/linkedin/avroutil1/compatibility/avro16/CodeTransformationsAvro16Test.java index 52251b002..609529685 100644 --- a/helper/tests/helper-tests-16/src/test/java/com/linkedin/avroutil1/compatibility/avro16/CodeTransformationsAvro16Test.java +++ b/helper/tests/helper-tests-16/src/test/java/com/linkedin/avroutil1/compatibility/avro16/CodeTransformationsAvro16Test.java @@ -6,16 +6,15 @@ package com.linkedin.avroutil1.compatibility.avro16; -import com.linkedin.avroutil1.testcommon.TestUtil; import com.linkedin.avroutil1.compatibility.AvroCompatibilityHelper; import com.linkedin.avroutil1.compatibility.AvroVersion; import com.linkedin.avroutil1.compatibility.CodeTransformations; +import com.linkedin.avroutil1.testcommon.TestUtil; import java.io.File; import java.io.FileInputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.regex.Matcher; - import net.openhft.compiler.CompilerUtils; import org.apache.avro.Schema; import org.apache.avro.compiler.specific.SpecificCompiler; @@ -51,6 +50,52 @@ public void testTransformAvro16HugeRecord() throws Exception { Assert.assertNotNull(transformedClass); } + @Test + public void testEnhanceNumericPutMethod() throws Exception { + // Use the IntsAndLongs schema for testing + String avsc = TestUtil.load("IntsAndLongs.avsc"); + Schema schema = AvroCompatibilityHelper.parse(avsc); + String originalCode = runNativeCodegen(schema); + + // Apply the transformation + String enhancedCode = CodeTransformations.enhanceNumericPutMethod(originalCode); + + // Verify the enhanced code contains the type checking for numeric conversions + Assert.assertTrue(enhancedCode.contains("if (value$ instanceof java.lang.Long)")); + Assert.assertTrue(enhancedCode.contains("if (value$ instanceof java.lang.Integer)")); + + // Compile the enhanced code to verify it's valid Java + try { + CompilerUtils.CACHED_COMPILER.loadFromJava(schema.getFullName(), enhancedCode); + } catch (Exception e) { + Assert.fail("Enhanced put method code should compile without errors"); + } + } + + @Test + public void testAddOverloadedNumericSetterMethods() throws Exception { + // Use the IntsAndLongs schema for testing + String avsc = TestUtil.load("IntsAndLongs.avsc"); + Schema schema = AvroCompatibilityHelper.parse(avsc); + String originalCode = runNativeCodegen(schema); + + // Apply the transformation + String enhancedCode = CodeTransformations.addOverloadedNumericSetterMethods(originalCode); + + // Verify the enhanced code contains the overloaded setters + Assert.assertTrue(enhancedCode.contains("public void setIntField(java.lang.Long value)")); + Assert.assertTrue(enhancedCode.contains("public void setLongField(java.lang.Integer value)")); + Assert.assertTrue(enhancedCode.contains("public void setBoxedIntField(java.lang.Long value)")); + Assert.assertTrue(enhancedCode.contains("public void setBoxedLongField(java.lang.Integer value)")); + + // Compile the enhanced code to verify it's valid Java + try { + CompilerUtils.CACHED_COMPILER.loadFromJava(schema.getFullName(), enhancedCode); + } catch (Exception e) { + Assert.fail("Enhanced setter methods code should compile without errors"); + } + } + private String runNativeCodegen(Schema schema) throws Exception { File outputRoot = Files.createTempDirectory(null).toFile(); SpecificCompiler compiler = new SpecificCompiler(schema); diff --git a/helper/tests/helper-tests-17/src/test/java/com/linkedin/avroutil1/compatibility/avro17/CodeTransformationsAvro17Test.java b/helper/tests/helper-tests-17/src/test/java/com/linkedin/avroutil1/compatibility/avro17/CodeTransformationsAvro17Test.java index f44dc5cf5..7c91a14f4 100644 --- a/helper/tests/helper-tests-17/src/test/java/com/linkedin/avroutil1/compatibility/avro17/CodeTransformationsAvro17Test.java +++ b/helper/tests/helper-tests-17/src/test/java/com/linkedin/avroutil1/compatibility/avro17/CodeTransformationsAvro17Test.java @@ -6,16 +6,15 @@ package com.linkedin.avroutil1.compatibility.avro17; -import com.linkedin.avroutil1.testcommon.TestUtil; import com.linkedin.avroutil1.compatibility.AvroCompatibilityHelper; import com.linkedin.avroutil1.compatibility.AvroVersion; import com.linkedin.avroutil1.compatibility.CodeTransformations; +import com.linkedin.avroutil1.testcommon.TestUtil; import java.io.File; import java.io.FileInputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.regex.Matcher; - import net.openhft.compiler.CompilerUtils; import org.apache.avro.Schema; import org.apache.avro.compiler.specific.SpecificCompiler; @@ -51,6 +50,80 @@ public void testTransformAvro17HugeRecord() throws Exception { Assert.assertNotNull(transformedClass); } + @Test + public void testEnhanceNumericPutMethod() throws Exception { + // Use the IntsAndLongs schema for testing + String avsc = TestUtil.load("IntsAndLongs.avsc"); + Schema schema = AvroCompatibilityHelper.parse(avsc); + String originalCode = runNativeCodegen(schema); + + // Apply the transformation + String enhancedCode = CodeTransformations.enhanceNumericPutMethod(originalCode); + + // Verify the enhanced code contains the type checking for numeric conversions + Assert.assertTrue(enhancedCode.contains("if (value$ instanceof java.lang.Long)")); + Assert.assertTrue(enhancedCode.contains("if (value$ instanceof java.lang.Integer)")); + + // Compile the enhanced code to verify it's valid Java + try { + CompilerUtils.CACHED_COMPILER.loadFromJava(schema.getFullName(), enhancedCode); + } catch (Exception e) { + Assert.fail("Enhanced put method code should compile without errors"); + } + } + + @Test + public void testAddOverloadedNumericSetterMethods() throws Exception { + // Use the IntsAndLongs schema for testing + String avsc = TestUtil.load("IntsAndLongs.avsc"); + Schema schema = AvroCompatibilityHelper.parse(avsc); + String originalCode = runNativeCodegen(schema); + + // Apply the transformation + String enhancedCode = CodeTransformations.addOverloadedNumericSetterMethods(originalCode); + + // Verify the enhanced code contains the overloaded setters + Assert.assertTrue(enhancedCode.contains("public void setIntField(java.lang.Long value)")); + Assert.assertTrue(enhancedCode.contains("public void setLongField(java.lang.Integer value)")); + Assert.assertTrue(enhancedCode.contains("public void setBoxedIntField(java.lang.Long value)")); + Assert.assertTrue(enhancedCode.contains("public void setBoxedLongField(java.lang.Integer value)")); + + // Compile the enhanced code to verify it's valid Java + try { + CompilerUtils.CACHED_COMPILER.loadFromJava(schema.getFullName(), enhancedCode); + } catch (Exception e) { + Assert.fail("Enhanced setter methods code should compile without errors"); + } + } + + @Test + public void testAddOverloadedNumericConstructor() throws Exception { + // Use the IntsAndLongs schema for testing + String avsc = TestUtil.load("IntsAndLongs.avsc"); + Schema schema = AvroCompatibilityHelper.parse(avsc); + String originalCode = runNativeCodegen(schema); + + // Apply the transformation + String enhancedCode = CodeTransformations.addOverloadedNumericConstructor(originalCode); + + // Verify the enhanced code contains the overloaded constructor with swapped types + Assert.assertTrue(enhancedCode.contains("public " + schema.getName() + "(java.lang.Integer longField, java.lang.Long " + + "intField, java.lang.Integer boxedLongField, java.lang.Long boxedIntField)")); + + // Compile the enhanced code to verify it's valid Java + Class generatedClass = null; + try { + generatedClass = CompilerUtils.CACHED_COMPILER.loadFromJava(schema.getFullName(), enhancedCode); + } catch (Exception e) { + Assert.fail("Enhanced constructor code should compile without errors"); + } + + Assert.assertNotNull(generatedClass.getConstructor( + java.lang.Long.class, java.lang.Integer.class, java.lang.Long.class, java.lang.Integer.class)); + Assert.assertNotNull(generatedClass.getConstructor( + java.lang.Integer.class, java.lang.Long.class, java.lang.Integer.class, java.lang.Long.class)); + } + private String runNativeCodegen(Schema schema) throws Exception { File outputRoot = Files.createTempDirectory(null).toFile(); SpecificCompiler compiler = new SpecificCompiler(schema); diff --git a/helper/tests/helper-tests-18/src/test/java/com/linkedin/avroutil1/compatibility/avro18/CodeTransformationsAvro18Test.java b/helper/tests/helper-tests-18/src/test/java/com/linkedin/avroutil1/compatibility/avro18/CodeTransformationsAvro18Test.java index 006a8cffd..fd897cacd 100644 --- a/helper/tests/helper-tests-18/src/test/java/com/linkedin/avroutil1/compatibility/avro18/CodeTransformationsAvro18Test.java +++ b/helper/tests/helper-tests-18/src/test/java/com/linkedin/avroutil1/compatibility/avro18/CodeTransformationsAvro18Test.java @@ -6,23 +6,21 @@ package com.linkedin.avroutil1.compatibility.avro18; -import com.linkedin.avroutil1.testcommon.TestUtil; import com.linkedin.avroutil1.compatibility.AvroCompatibilityHelper; import com.linkedin.avroutil1.compatibility.AvroVersion; import com.linkedin.avroutil1.compatibility.CodeTransformations; +import com.linkedin.avroutil1.testcommon.TestUtil; import java.io.File; import java.io.FileInputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.regex.Matcher; - import net.openhft.compiler.CompilerUtils; import org.apache.avro.Schema; import org.apache.avro.compiler.specific.SpecificCompiler; import org.apache.commons.io.IOUtils; import org.testng.Assert; import org.testng.annotations.Test; - import under18wbuildersmin18.NormalRecordWithoutReferences; @@ -58,6 +56,80 @@ public void testBuilders() throws Exception { Assert.assertNotNull(NormalRecordWithoutReferences.newBuilder()); } + @Test + public void testEnhanceNumericPutMethod() throws Exception { + // Use the IntsAndLongs schema for testing + String avsc = TestUtil.load("IntsAndLongs.avsc"); + Schema schema = AvroCompatibilityHelper.parse(avsc); + String originalCode = runNativeCodegen(schema); + + // Apply the transformation + String enhancedCode = CodeTransformations.enhanceNumericPutMethod(originalCode); + + // Verify the enhanced code contains the type checking for numeric conversions + Assert.assertTrue(enhancedCode.contains("if (value$ instanceof java.lang.Long)")); + Assert.assertTrue(enhancedCode.contains("if (value$ instanceof java.lang.Integer)")); + + // Compile the enhanced code to verify it's valid Java + try { + CompilerUtils.CACHED_COMPILER.loadFromJava(schema.getFullName(), enhancedCode); + } catch (Exception e) { + Assert.fail("Enhanced put method code should compile without errors"); + } + } + + @Test + public void testAddOverloadedNumericSetterMethods() throws Exception { + // Use the IntsAndLongs schema for testing + String avsc = TestUtil.load("IntsAndLongs.avsc"); + Schema schema = AvroCompatibilityHelper.parse(avsc); + String originalCode = runNativeCodegen(schema); + + // Apply the transformation + String enhancedCode = CodeTransformations.addOverloadedNumericSetterMethods(originalCode); + + // Verify the enhanced code contains the overloaded setters + Assert.assertTrue(enhancedCode.contains("public void setIntField(java.lang.Long value)")); + Assert.assertTrue(enhancedCode.contains("public void setLongField(java.lang.Integer value)")); + Assert.assertTrue(enhancedCode.contains("public void setBoxedIntField(java.lang.Long value)")); + Assert.assertTrue(enhancedCode.contains("public void setBoxedLongField(java.lang.Integer value)")); + + // Compile the enhanced code to verify it's valid Java + try { + CompilerUtils.CACHED_COMPILER.loadFromJava(schema.getFullName(), enhancedCode); + } catch (Exception e) { + Assert.fail("Enhanced setter methods code should compile without errors"); + } + } + + @Test + public void testAddOverloadedNumericConstructor() throws Exception { + // Use the IntsAndLongs schema for testing + String avsc = TestUtil.load("IntsAndLongs.avsc"); + Schema schema = AvroCompatibilityHelper.parse(avsc); + String originalCode = runNativeCodegen(schema); + + // Apply the transformation + String enhancedCode = CodeTransformations.addOverloadedNumericConstructor(originalCode); + + // Verify the enhanced code contains the overloaded constructor with swapped types + Assert.assertTrue(enhancedCode.contains("public " + schema.getName() + "(java.lang.Integer longField, java.lang.Long " + + "intField, java.lang.Integer boxedLongField, java.lang.Long boxedIntField)")); + + // Compile the enhanced code to verify it's valid Java + Class generatedClass = null; + try { + generatedClass = CompilerUtils.CACHED_COMPILER.loadFromJava(schema.getFullName(), enhancedCode); + } catch (Exception e) { + Assert.fail("Enhanced constructor code should compile without errors"); + } + + Assert.assertNotNull(generatedClass.getConstructor( + java.lang.Long.class, java.lang.Integer.class, java.lang.Long.class, java.lang.Integer.class)); + Assert.assertNotNull(generatedClass.getConstructor( + java.lang.Integer.class, java.lang.Long.class, java.lang.Integer.class, java.lang.Long.class)); + } + private String runNativeCodegen(Schema schema) throws Exception { File outputRoot = Files.createTempDirectory(null).toFile(); SpecificCompiler compiler = new SpecificCompiler(schema); diff --git a/helper/tests/helper-tests-19/src/test/java/com/linkedin/avroutil1/compatibility/avro19/CodeTransformationsAvro19Test.java b/helper/tests/helper-tests-19/src/test/java/com/linkedin/avroutil1/compatibility/avro19/CodeTransformationsAvro19Test.java index 79f98b38a..bf4c730ed 100644 --- a/helper/tests/helper-tests-19/src/test/java/com/linkedin/avroutil1/compatibility/avro19/CodeTransformationsAvro19Test.java +++ b/helper/tests/helper-tests-19/src/test/java/com/linkedin/avroutil1/compatibility/avro19/CodeTransformationsAvro19Test.java @@ -6,17 +6,16 @@ package com.linkedin.avroutil1.compatibility.avro19; -import com.linkedin.avroutil1.testcommon.TestUtil; import com.linkedin.avroutil1.compatibility.AvroCompatibilityHelper; import com.linkedin.avroutil1.compatibility.AvroVersion; import com.linkedin.avroutil1.compatibility.CodeTransformations; +import com.linkedin.avroutil1.testcommon.TestUtil; import java.io.File; import java.io.FileInputStream; import java.lang.reflect.Method; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.regex.Matcher; - import net.openhft.compiler.CompilerUtils; import org.apache.avro.Schema; import org.apache.avro.compiler.specific.SpecificCompiler; @@ -24,7 +23,6 @@ import org.apache.commons.io.IOUtils; import org.testng.Assert; import org.testng.annotations.Test; - import under19wbuildersmin18.NormalRecordWithoutReferences; @@ -146,6 +144,80 @@ public void testBuilders() throws Exception { Assert.assertNotNull(NormalRecordWithoutReferences.newBuilder()); } + @Test + public void testEnhanceNumericPutMethod() throws Exception { + // Use the IntsAndLongs schema for testing + String avsc = TestUtil.load("IntsAndLongs.avsc"); + Schema schema = AvroCompatibilityHelper.parse(avsc); + String originalCode = runNativeCodegen(schema); + + // Apply the transformation + String enhancedCode = CodeTransformations.enhanceNumericPutMethod(originalCode); + + // Verify the enhanced code contains the type checking for numeric conversions + Assert.assertTrue(enhancedCode.contains("if (value$ instanceof java.lang.Long)")); + Assert.assertTrue(enhancedCode.contains("if (value$ instanceof java.lang.Integer)")); + + // Compile the enhanced code to verify it's valid Java + try { + CompilerUtils.CACHED_COMPILER.loadFromJava(schema.getFullName(), enhancedCode); + } catch (Exception e) { + Assert.fail("Enhanced put method code should compile without errors"); + } + } + + @Test + public void testAddOverloadedNumericSetterMethods() throws Exception { + // Use the IntsAndLongs schema for testing + String avsc = TestUtil.load("IntsAndLongs.avsc"); + Schema schema = AvroCompatibilityHelper.parse(avsc); + String originalCode = runNativeCodegen(schema); + + // Apply the transformation + String enhancedCode = CodeTransformations.addOverloadedNumericSetterMethods(originalCode); + + // Verify the enhanced code contains the overloaded setters + Assert.assertTrue(enhancedCode.contains("public void setIntField(long value)")); + Assert.assertTrue(enhancedCode.contains("public void setLongField(int value)")); + Assert.assertTrue(enhancedCode.contains("public void setBoxedIntField(java.lang.Long value)")); + Assert.assertTrue(enhancedCode.contains("public void setBoxedLongField(java.lang.Integer value)")); + + // Compile the enhanced code to verify it's valid Java + try { + CompilerUtils.CACHED_COMPILER.loadFromJava(schema.getFullName(), enhancedCode); + } catch (Exception e) { + Assert.fail("Enhanced setter methods code should compile without errors"); + } + } + + @Test + public void testAddOverloadedNumericConstructor() throws Exception { + // Use the IntsAndLongs schema for testing + String avsc = TestUtil.load("IntsAndLongs.avsc"); + Schema schema = AvroCompatibilityHelper.parse(avsc); + String originalCode = runNativeCodegen(schema); + + // Apply the transformation + String enhancedCode = CodeTransformations.addOverloadedNumericConstructor(originalCode); + + // Verify the enhanced code contains the overloaded constructor with swapped types + Assert.assertTrue(enhancedCode.contains("public " + schema.getName() + "(java.lang.Integer longField, java.lang.Long " + + "intField, java.lang.Integer boxedLongField, java.lang.Long boxedIntField)")); + + // Compile the enhanced code to verify it's valid Java + Class generatedClass = null; + try { + generatedClass = CompilerUtils.CACHED_COMPILER.loadFromJava(schema.getFullName(), enhancedCode); + } catch (Exception e) { + Assert.fail("Enhanced constructor code should compile without errors"); + } + + Assert.assertNotNull(generatedClass.getConstructor( + java.lang.Long.class, java.lang.Integer.class, java.lang.Long.class, java.lang.Integer.class)); + Assert.assertNotNull(generatedClass.getConstructor( + java.lang.Integer.class, java.lang.Long.class, java.lang.Integer.class, java.lang.Long.class)); + } + private String runNativeCodegen(Schema schema) throws Exception { File outputRoot = Files.createTempDirectory(null).toFile(); SpecificCompiler compiler = new SpecificCompiler(schema); diff --git a/helper/tests/helper-tests-common/src/main/resources/IntsAndLongs.avsc b/helper/tests/helper-tests-common/src/main/resources/IntsAndLongs.avsc new file mode 100644 index 000000000..e80229e9f --- /dev/null +++ b/helper/tests/helper-tests-common/src/main/resources/IntsAndLongs.avsc @@ -0,0 +1,25 @@ +{ + "type": "record", + "namespace": "com.acme", + "name": "IntsAndLongs", + "fields": [ + { + "name": "longField", + "type": "long" + }, + { + "name": "intField", + "type": "int" + }, + { + "name": "boxedLongField", + "type": ["null", "long"], + "default": null + }, + { + "name": "boxedIntField", + "type": ["null", "int"], + "default": null + } + ] +}