From 828d2e756ef5ea8c11818d266ca303519bc0b699 Mon Sep 17 00:00:00 2001 From: "github-classroom[bot]" <66690702+github-classroom[bot]@users.noreply.github.com> Date: Sat, 4 Oct 2025 18:51:18 +0000 Subject: [PATCH 1/8] add deadline --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 87ffcbb..1a6a1a9 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ +[![Review Assignment Due Date](https://classroom.github.com/assets/deadline-readme-button-22041afd0340ce965d47ae6ef1cefeee28c7c493a6346c4f15d667ab976d596c.svg)](https://classroom.github.com/a/4ISSpVK4) **Java reflection** Ваша задача --- написать генератор экземпляров произвольных классов. From 9443eef55fb8d0c127b130bf7570b21fe88adc03 Mon Sep 17 00:00:00 2001 From: Karim Khasan Date: Tue, 4 Nov 2025 19:39:10 +0300 Subject: [PATCH 2/8] feat: implement class generator --- annotation-processor/build.gradle.kts | 14 ++ .../processor/GeneratableProcessor.java | 91 +++++++++ .../javax.annotation.processing.Processor | 1 + build.gradle.kts | 3 + core/build.gradle.kts | 13 ++ .../org/example/annotation/Generatable.java | 13 ++ processor/build.gradle.kts | 21 ++ settings.gradle.kts | 4 +- .../java/org/example/GenerateExample.java | 9 +- .../org/example/classes/BinaryTreeNode.java | 12 ++ src/main/java/org/example/classes/Cart.java | 10 + .../java/org/example/classes/Example.java | 7 +- .../java/org/example/classes/Product.java | 19 +- .../java/org/example/classes/Rectangle.java | 11 + .../java/org/example/classes/Triangle.java | 12 ++ .../generator/AbstractValueGenerator.java | 26 +++ .../java/org/example/generator/Generator.java | 191 +++++++++++++++++- .../generator/PrimitiveValueGenerator.java | 43 ++++ .../generator/StringValueGenerator.java | 29 +++ .../org/example/generator/ValueGenerator.java | 8 + .../org/example/generator/GeneratorTest.java | 170 ++++++++++++++++ 21 files changed, 687 insertions(+), 20 deletions(-) create mode 100644 annotation-processor/build.gradle.kts create mode 100644 annotation-processor/src/main/java/org/example/annotation/processor/GeneratableProcessor.java create mode 100644 annotation-processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor create mode 100644 core/build.gradle.kts create mode 100644 core/src/main/java/org/example/annotation/Generatable.java create mode 100644 processor/build.gradle.kts create mode 100644 src/main/java/org/example/generator/AbstractValueGenerator.java create mode 100644 src/main/java/org/example/generator/PrimitiveValueGenerator.java create mode 100644 src/main/java/org/example/generator/StringValueGenerator.java create mode 100644 src/main/java/org/example/generator/ValueGenerator.java create mode 100644 src/test/java/org/example/generator/GeneratorTest.java diff --git a/annotation-processor/build.gradle.kts b/annotation-processor/build.gradle.kts new file mode 100644 index 0000000..aca3885 --- /dev/null +++ b/annotation-processor/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + id("java") +} + +group = "org.example" +version = "1.0-SNAPSHOT" + +repositories { + mavenCentral() +} + +dependencies { + implementation(project(":core")) +} diff --git a/annotation-processor/src/main/java/org/example/annotation/processor/GeneratableProcessor.java b/annotation-processor/src/main/java/org/example/annotation/processor/GeneratableProcessor.java new file mode 100644 index 0000000..9385f41 --- /dev/null +++ b/annotation-processor/src/main/java/org/example/annotation/processor/GeneratableProcessor.java @@ -0,0 +1,91 @@ +package org.example.annotation.processor; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.Writer; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.annotation.processing.SupportedSourceVersion; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.TypeElement; +import javax.tools.Diagnostic; +import javax.tools.JavaFileObject; + +import org.example.annotation.Generatable; + +@SupportedSourceVersion(SourceVersion.RELEASE_21) +@SupportedAnnotationTypes("org.example.annotation.Generatable") +public class GeneratableProcessor extends AbstractProcessor { + private boolean hasGenerated = false; + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + if (hasGenerated || roundEnv.processingOver()) { + return true; + } + + var annotatedElements = roundEnv.getElementsAnnotatedWith(Generatable.class); + + var typeElements = annotatedElements.stream() + .map(this::getTypeElement) + .filter(Objects::nonNull) + .toList(); + + if (!typeElements.isEmpty()) { + generateGeneratableRegistry(typeElements); + hasGenerated = true; + } + + return true; + } + + private TypeElement getTypeElement(Element element) { + return (element.getKind() == ElementKind.CLASS) ? (TypeElement) element : null; + } + + private void generateGeneratableRegistry(List typeElements) { + try { + JavaFileObject file = processingEnv.getFiler() + .createSourceFile("org.example.generator.GeneratableRegistry"); + + try (Writer writer = new BufferedWriter(file.openWriter())) { + writer.write("package org.example.generator;\n\n"); + writer.write("import java.util.List;\n\n"); + writer.write("// Auto-generated by GeneratableProcessor. Don't modify this code!\n"); + writer.write("public class GeneratableRegistry {\n"); + writer.write(" public static List> getAllGeneratableClasses() {\n"); + writer.write(" return List.of(\n"); + + for (int i = 0; i < typeElements.size(); i++) { + writer.write(" " + typeElements.get(i).getQualifiedName().toString() + ".class"); + if (i < typeElements.size() - 1) { + writer.write(",\n"); + } else { + writer.write("\n"); + } + } + + writer.write(" );\n"); + writer.write(" }\n"); + writer.write("}\n"); + } + + processingEnv.getMessager().printMessage( + Diagnostic.Kind.NOTE, + "Successfully generated GeneratableRegistry with " + typeElements.size() + " classes" + ); + } catch (IOException e) { + processingEnv.getMessager().printMessage( + Diagnostic.Kind.ERROR, + "Failed to generate registry: " + e.getMessage() + ); + } + } +} diff --git a/annotation-processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/annotation-processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor new file mode 100644 index 0000000..e468d11 --- /dev/null +++ b/annotation-processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor @@ -0,0 +1 @@ +org.example.annotation.processor.GeneratableProcessor \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index b4738ae..0e87e53 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,6 +10,9 @@ repositories { } dependencies { + implementation(project(":core")) + annotationProcessor(project(":annotation-processor")) + testImplementation(platform("org.junit:junit-bom:5.10.0")) testImplementation("org.junit.jupiter:junit-jupiter") testRuntimeOnly("org.junit.platform:junit-platform-launcher") diff --git a/core/build.gradle.kts b/core/build.gradle.kts new file mode 100644 index 0000000..812f64a --- /dev/null +++ b/core/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + id("java") +} + +group = "org.example" +version = "1.0-SNAPSHOT" + +repositories { + mavenCentral() +} + +dependencies { +} diff --git a/core/src/main/java/org/example/annotation/Generatable.java b/core/src/main/java/org/example/annotation/Generatable.java new file mode 100644 index 0000000..948f020 --- /dev/null +++ b/core/src/main/java/org/example/annotation/Generatable.java @@ -0,0 +1,13 @@ +package org.example.annotation; + +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; + +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface Generatable { +} diff --git a/processor/build.gradle.kts b/processor/build.gradle.kts new file mode 100644 index 0000000..22447dd --- /dev/null +++ b/processor/build.gradle.kts @@ -0,0 +1,21 @@ +plugins { + id("java") +} + +group = "org.example" +version = "1.0-SNAPSHOT" + +repositories { + mavenCentral() +} + +dependencies { + compileOnly(":reflection-template") + + testImplementation(platform("org.junit:junit-bom:5.10.0")) + testImplementation("org.junit.jupiter:junit-jupiter") +} + +tasks.test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index af5b8ff..b28bd35 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1 +1,3 @@ -rootProject.name = "reflection-template" \ No newline at end of file +rootProject.name = "reflection-template" +include("annotation-processor") +include("core") diff --git a/src/main/java/org/example/GenerateExample.java b/src/main/java/org/example/GenerateExample.java index 47679a9..c448574 100644 --- a/src/main/java/org/example/GenerateExample.java +++ b/src/main/java/org/example/GenerateExample.java @@ -1,12 +1,19 @@ package org.example; +import java.util.List; +import java.util.Random; + import org.example.classes.Example; import org.example.generator.Generator; +import org.example.generator.PrimitiveValueGenerator; +import org.example.generator.StringValueGenerator; public class GenerateExample { public static void main(String[] args) { - var gen = new Generator(); + var random = new Random(42); + var gen = new Generator(random, List.of(new PrimitiveValueGenerator(random), new StringValueGenerator(random))); + try { Object generated = gen.generateValueOfType(Example.class); System.out.println(generated); diff --git a/src/main/java/org/example/classes/BinaryTreeNode.java b/src/main/java/org/example/classes/BinaryTreeNode.java index 046ff56..431197d 100644 --- a/src/main/java/org/example/classes/BinaryTreeNode.java +++ b/src/main/java/org/example/classes/BinaryTreeNode.java @@ -1,5 +1,8 @@ package org.example.classes; +import org.example.annotation.Generatable; + +@Generatable public class BinaryTreeNode { private Integer data; private BinaryTreeNode left; @@ -30,4 +33,13 @@ public void setLeft(BinaryTreeNode left) { public void setRight(BinaryTreeNode right) { this.right = right; } + + @Override + public String toString() { + return "BinaryTreeNode{" + + "data=" + data + + ", left=" + left + + ", right=" + right + + '}'; + } } diff --git a/src/main/java/org/example/classes/Cart.java b/src/main/java/org/example/classes/Cart.java index 965237d..1238937 100644 --- a/src/main/java/org/example/classes/Cart.java +++ b/src/main/java/org/example/classes/Cart.java @@ -2,6 +2,9 @@ import java.util.List; +import org.example.annotation.Generatable; + +@Generatable public class Cart { private List items; @@ -18,4 +21,11 @@ public void setItems(List items) { } // Конструктор, методы добавления и удаления товаров, геттеры и другие методы + + @Override + public String toString() { + return "Cart{" + + "items=" + items + + '}'; + } } \ No newline at end of file diff --git a/src/main/java/org/example/classes/Example.java b/src/main/java/org/example/classes/Example.java index eac9463..39d401f 100644 --- a/src/main/java/org/example/classes/Example.java +++ b/src/main/java/org/example/classes/Example.java @@ -1,5 +1,8 @@ package org.example.classes; +import org.example.annotation.Generatable; + +@Generatable public class Example { int i; @@ -9,6 +12,8 @@ public Example(int i) { @Override public String toString() { - return "Example(" + i + ")"; + return "Example{" + + "i=" + i + + '}'; } } diff --git a/src/main/java/org/example/classes/Product.java b/src/main/java/org/example/classes/Product.java index e7dcc89..e183bc9 100644 --- a/src/main/java/org/example/classes/Product.java +++ b/src/main/java/org/example/classes/Product.java @@ -1,5 +1,8 @@ package org.example.classes; +import org.example.annotation.Generatable; + +@Generatable public class Product { private String name; private double price; @@ -30,19 +33,11 @@ public void setPrice(double price) { this.price = price; } - @Override - public int hashCode() { - return super.hashCode(); - } - - @Override - public boolean equals(Object obj) { - return super.equals(obj); - } - @Override public String toString() { - return super.toString(); + return "Product{" + + "name='" + name + '\'' + + ", price=" + price + + '}'; } - } diff --git a/src/main/java/org/example/classes/Rectangle.java b/src/main/java/org/example/classes/Rectangle.java index 90b0886..a57e51a 100644 --- a/src/main/java/org/example/classes/Rectangle.java +++ b/src/main/java/org/example/classes/Rectangle.java @@ -1,5 +1,8 @@ package org.example.classes; +import org.example.annotation.Generatable; + +@Generatable public class Rectangle implements Shape { private double length; private double width; @@ -18,4 +21,12 @@ public double getArea() { public double getPerimeter() { return 2 * (length + width); } + + @Override + public String toString() { + return "Rectangle{" + + "length=" + length + + ", width=" + width + + '}'; + } } \ No newline at end of file diff --git a/src/main/java/org/example/classes/Triangle.java b/src/main/java/org/example/classes/Triangle.java index 011e96f..67cba9a 100644 --- a/src/main/java/org/example/classes/Triangle.java +++ b/src/main/java/org/example/classes/Triangle.java @@ -1,5 +1,8 @@ package org.example.classes; +import org.example.annotation.Generatable; + +@Generatable public class Triangle implements Shape { private double sideA; private double sideB; @@ -21,4 +24,13 @@ public double getArea() { public double getPerimeter() { return sideA + sideB + sideC; } + + @Override + public String toString() { + return "Triangle{" + + "sideA=" + sideA + + ", sideB=" + sideB + + ", sideC=" + sideC + + '}'; + } } \ No newline at end of file diff --git a/src/main/java/org/example/generator/AbstractValueGenerator.java b/src/main/java/org/example/generator/AbstractValueGenerator.java new file mode 100644 index 0000000..28d15af --- /dev/null +++ b/src/main/java/org/example/generator/AbstractValueGenerator.java @@ -0,0 +1,26 @@ +package org.example.generator; + +import java.lang.reflect.Type; +import java.util.Map; +import java.util.Random; +import java.util.function.Function; + +public abstract class AbstractValueGenerator implements ValueGenerator { + private final Random random; + + protected AbstractValueGenerator(Random random) { + this.random = random; + } + + @Override + public boolean supports(Class clazz) { + return getMappingFunction().containsKey(clazz); + } + + @Override + public Object generate(Class clazz, Type genericType) { + return getMappingFunction().get(clazz).apply(random); + } + + protected abstract Map, Function> getMappingFunction(); +} diff --git a/src/main/java/org/example/generator/Generator.java b/src/main/java/org/example/generator/Generator.java index 9d86bfb..36f6a26 100644 --- a/src/main/java/org/example/generator/Generator.java +++ b/src/main/java/org/example/generator/Generator.java @@ -1,18 +1,199 @@ package org.example.generator; import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Modifier; +import java.lang.reflect.Parameter; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; import java.util.Random; +import java.util.Set; + +import org.example.annotation.Generatable; public class Generator { + public static final int MAX_COLLECTION_SIZE = 5; + public static final int MAX_RECURSION_DEPTH = 3; + private final Random random; + private final List valueGenerators; + + public Generator( + Random random, + List valueGenerators + ) { + this.random = random; + this.valueGenerators = valueGenerators; + } + + public Object generateValueOfType(Class clazz) { + try { + return generateValueOfTypeInternal(clazz, null, 0); + } catch (Exception e) { + throw new RuntimeException("Failed to generate value for type: " + clazz.getName(), e); + } + } + + private Object generateValueOfTypeInternal(Class clazz, Type genericType, int depth) throws Exception { + var valueGenerator = findValueGeneratorForClass(clazz); + if (valueGenerator != null) { + return valueGenerator.generate(clazz, genericType); + } + + if (depth > MAX_RECURSION_DEPTH) { + return null; + } + + if (List.class.isAssignableFrom(clazz)) { + return generateList(genericType, depth); + } + if (Set.class.isAssignableFrom(clazz)) { + return generateSet(genericType, depth); + } + if (Map.class.isAssignableFrom(clazz)) { + return generateMap(genericType, depth); + } + + if (clazz.isInterface() || Modifier.isAbstract(clazz.getModifiers())) { + return generateInterfaceImplementation(clazz, depth); + } + + if (!clazz.isAnnotationPresent(Generatable.class)) { + throw new IllegalArgumentException("Class " + clazz.getName() + " is not annotated with @Generatable"); + } + + Constructor constructor = selectConstructor(clazz); + Object[] params = generateParameters(constructor, depth + 1); + return constructor.newInstance(params); + } + + private ValueGenerator findValueGeneratorForClass(Class clazz) { + return valueGenerators.stream() + .filter(it -> it.supports(clazz)) + .findFirst() + .orElse(null); + } + + private List generateList(Type genericType, int depth) throws Exception { + Class elementType = getCollectionElementType(genericType); + + int size = random.nextInt(MAX_COLLECTION_SIZE + 1); + List list = new ArrayList<>(); + for (int i = 0; i < size; i++) { + Object element = generateValueOfTypeInternal(elementType, null, depth); + list.add(element); + } + return list; + } + + private Set generateSet(Type genericType, int depth) throws Exception { + Class elementType = getCollectionElementType(genericType); + + int size = random.nextInt(MAX_COLLECTION_SIZE + 1); + Set set = new HashSet<>(); + for (int i = 0; i < size; i++) { + Object element = generateValueOfTypeInternal(elementType, null, depth); + set.add(element); + } + return set; + } + + private Class getCollectionElementType(Type genericType) { + Class elementType = Object.class; - public Object generateValueOfType(Class clazz) throws InvocationTargetException, InstantiationException, IllegalAccessException { + if (genericType instanceof ParameterizedType paramType) { + Type[] typeArgs = paramType.getActualTypeArguments(); + if (typeArgs.length > 0) { + elementType = getClassFromType(typeArgs[0]); + } + } + return elementType; + } + + private Map generateMap(Type genericType, int depth) throws Exception { + Class keyType = Object.class; + Class valueType = Object.class; + + if (genericType instanceof ParameterizedType paramType) { + Type[] typeArgs = paramType.getActualTypeArguments(); + if (typeArgs.length > 0) { + keyType = getClassFromType(typeArgs[0]); + } + if (typeArgs.length > 1) { + valueType = getClassFromType(typeArgs[1]); + } + } + + int size = random.nextInt(MAX_COLLECTION_SIZE + 1); + Map map = new HashMap<>(); + for (int i = 0; i < size; i++) { + Object key = generateValueOfTypeInternal(keyType, null, depth); + Object value = generateValueOfTypeInternal(valueType, null, depth); + map.put(key, value); + } + return map; + } + + private Class getClassFromType(Type type) { + if (type instanceof Class cls) { + return cls; + } else if (type instanceof ParameterizedType paramType) { + return (Class) paramType.getRawType(); + } + return Object.class; + } + + private Object generateInterfaceImplementation(Class interfaceClass, int depth) throws Exception { + List> implementations = findGeneratableImplementations(interfaceClass); + + if (implementations.isEmpty()) { + throw new IllegalArgumentException( + "No @Generatable implementations found for interface/abstract class: " + interfaceClass.getName() + ); + } + + Class selectedClass = implementations.get(random.nextInt(implementations.size())); + return generateValueOfTypeInternal(selectedClass, null, depth); + } + + private List> findGeneratableImplementations(Class interfaceClass) { + return GeneratableRegistry.getAllGeneratableClasses().stream() + .filter(it -> isGeneratableClass(it, interfaceClass)) + .toList(); + } + + private boolean isGeneratableClass(Class clazz, Class interfaceClass) { + return interfaceClass.isAssignableFrom(clazz) && + !clazz.isInterface() && + !Modifier.isAbstract(clazz.getModifiers()); + } + + private Constructor selectConstructor(Class clazz) { Constructor[] constructors = clazz.getDeclaredConstructors(); - int randomConstructorIndex = new Random().nextInt(constructors.length); - Constructor randomConstructor = constructors[randomConstructorIndex]; - return randomConstructor.newInstance(111); + if (constructors.length == 0) { + throw new IllegalArgumentException("No constructors found for class: " + clazz.getName()); + } + + int selectedIndex = random.nextInt(constructors.length); + + return constructors[selectedIndex]; } + private Object[] generateParameters(Constructor constructor, int depth) throws Exception { + Parameter[] parameters = constructor.getParameters(); + Object[] result = new Object[parameters.length]; + + for (int i = 0; i < parameters.length; i++) { + Type genericType = parameters[i].getParameterizedType(); + Class paramType = parameters[i].getType(); + result[i] = generateValueOfTypeInternal(paramType, genericType, depth); + } + + return result; + } } diff --git a/src/main/java/org/example/generator/PrimitiveValueGenerator.java b/src/main/java/org/example/generator/PrimitiveValueGenerator.java new file mode 100644 index 0000000..5e316f0 --- /dev/null +++ b/src/main/java/org/example/generator/PrimitiveValueGenerator.java @@ -0,0 +1,43 @@ +package org.example.generator; + +import java.util.Map; +import java.util.Random; +import java.util.function.Function; + +import static java.util.Map.entry; + +public class PrimitiveValueGenerator extends AbstractValueGenerator { + private static final Integer ALPHABET_SIZE = 26; + + private static final Map, Function> MAPPING_FUNCTION = Map.ofEntries( + entry(int.class, Random::nextInt), + entry(Integer.class, Random::nextInt), + entry(long.class, Random::nextLong), + entry(Long.class, Random::nextLong), + entry(double.class, (random) -> random.nextDouble() * 100), + entry(Double.class, (random) -> random.nextDouble() * 100), + entry(float.class, (random) -> random.nextFloat() * 100), + entry(Float.class, (random) -> random.nextFloat() * 100), + entry(short.class, (random) -> (short) random.nextInt(Short.MAX_VALUE)), + entry(Short.class, (random) -> (short) random.nextInt(Short.MAX_VALUE)), + entry(byte.class, (random) -> (byte) random.nextInt(Byte.MAX_VALUE)), + entry(Byte.class, (random) -> (byte) random.nextInt(Byte.MAX_VALUE)), + entry(boolean.class, Random::nextBoolean), + entry(Boolean.class, Random::nextBoolean), + entry(char.class, PrimitiveValueGenerator::nextChar), + entry(Character.class, PrimitiveValueGenerator::nextChar) + ); + + public PrimitiveValueGenerator(Random random) { + super(random); + } + + @Override + protected Map, Function> getMappingFunction() { + return MAPPING_FUNCTION; + } + + private static char nextChar(Random random) { + return (char) ('a' + random.nextInt(ALPHABET_SIZE)); + } +} diff --git a/src/main/java/org/example/generator/StringValueGenerator.java b/src/main/java/org/example/generator/StringValueGenerator.java new file mode 100644 index 0000000..3a5b954 --- /dev/null +++ b/src/main/java/org/example/generator/StringValueGenerator.java @@ -0,0 +1,29 @@ +package org.example.generator; + +import java.util.Map; +import java.util.Random; +import java.util.function.Function; + +public class StringValueGenerator extends AbstractValueGenerator { + private static final Integer MAX_STRING_LENGTH = 50; + + private static final Map, Function> MAPPING_FUNCTION = Map.of( + String.class, (random) -> StringValueGenerator.nextString(random, random.nextInt(MAX_STRING_LENGTH)) + ); + + public StringValueGenerator(Random random) { + super(random); + } + + @Override + protected Map, Function> getMappingFunction() { + return MAPPING_FUNCTION; + } + + private static String nextString(Random random, int length) { + return random.ints('a', 'z') + .limit(length) + .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append) + .toString(); + } +} diff --git a/src/main/java/org/example/generator/ValueGenerator.java b/src/main/java/org/example/generator/ValueGenerator.java new file mode 100644 index 0000000..e0586c4 --- /dev/null +++ b/src/main/java/org/example/generator/ValueGenerator.java @@ -0,0 +1,8 @@ +package org.example.generator; + +import java.lang.reflect.Type; + +public interface ValueGenerator { + boolean supports(Class clazz); + Object generate(Class clazz, Type genericType); +} diff --git a/src/test/java/org/example/generator/GeneratorTest.java b/src/test/java/org/example/generator/GeneratorTest.java new file mode 100644 index 0000000..de1d97e --- /dev/null +++ b/src/test/java/org/example/generator/GeneratorTest.java @@ -0,0 +1,170 @@ +package org.example.generator; + +import java.util.List; +import java.util.Random; + +import org.example.classes.BinaryTreeNode; +import org.example.classes.Cart; +import org.example.classes.Example; +import org.example.classes.Product; +import org.example.classes.Rectangle; +import org.example.classes.Shape; +import org.example.classes.Triangle; +import org.junit.jupiter.api.Test; + +import static org.example.generator.Generator.MAX_COLLECTION_SIZE; +import static org.example.generator.Generator.MAX_RECURSION_DEPTH; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class GeneratorTest { + private final Random random = new Random(42); + private final Generator generator = new Generator(random, List.of( + new PrimitiveValueGenerator(random), + new StringValueGenerator(random) + )); + + @Test + void generateExample() { + Object result = generator.generateValueOfType(Example.class); + + assertNotNull(result); + assertInstanceOf(Example.class, result); + + Example example = (Example) result; + System.out.println("Generated Example: " + example); + } + + @Test + void generateProduct() { + Object result = generator.generateValueOfType(Product.class); + + assertNotNull(result); + assertInstanceOf(Product.class, result); + + Product product = (Product) result; + assertNotNull(product.getName(), "Product name should not be null"); + + System.out.println("Generated Product: " + product); + } + + @Test + void generateRectangle() { + Object result = generator.generateValueOfType(Rectangle.class); + + assertNotNull(result); + assertInstanceOf(Rectangle.class, result); + + Rectangle rectangle = (Rectangle) result; + + System.out.println("Generated Rectangle: " + rectangle); + } + + @Test + void generateTriangle() { + Object result = generator.generateValueOfType(Triangle.class); + + assertNotNull(result); + assertInstanceOf(Triangle.class, result); + + Triangle triangle = (Triangle) result; + System.out.println("Generated Triangle: " + triangle); + } + + @Test + void generateCart() { + Object result = generator.generateValueOfType(Cart.class); + + assertNotNull(result); + assertInstanceOf(Cart.class, result); + + Cart cart = (Cart) result; + assertNotNull(cart.getItems(), "Cart items should not be null"); + assertTrue( + cart.getItems().size() <= MAX_COLLECTION_SIZE, + "Cart should have at most" + MAX_COLLECTION_SIZE + " items" + ); + + cart.getItems().forEach(item -> { + assertInstanceOf(Product.class, item, "Cart item should be Product"); + assertNotNull(item.getName()); + }); + + System.out.println("Generated Cart with " + cart.getItems().size() + " items:"); + cart.getItems().forEach(p -> System.out.println(" - " + p)); + } + + @Test + void generateShapeImplementation() { + Object result = generator.generateValueOfType(Shape.class); + + assertNotNull(result); + assertInstanceOf(Shape.class, result); + assertTrue(result instanceof Rectangle || result instanceof Triangle, + "Shape should be either Rectangle or Triangle"); + + Shape shape = (Shape) result; + assertTrue(shape.getArea() >= 0); + assertTrue(shape.getPerimeter() >= 0); + + System.out.println("Generated Shape: " + shape.getClass().getSimpleName() + " = " + shape); + } + + @Test + void generateBinaryTreeNode() { + Object result = generator.generateValueOfType(BinaryTreeNode.class); + + assertNotNull(result); + assertInstanceOf(BinaryTreeNode.class, result); + + BinaryTreeNode tree = (BinaryTreeNode) result; + assertNotNull(tree.getData(), "Root data should not be null"); + + int depth = calculateTreeDepth(tree); + assertTrue(depth <= MAX_RECURSION_DEPTH + 1, "Tree depth should be limited"); + + System.out.println("Generated BinaryTreeNode with depth " + depth + ":"); + printTree(tree, "", true); + } + + @Test + void generateNotGeneratableThrows() { + class NotGeneratable { + public NotGeneratable(int x) {} + } + + assertThrows(RuntimeException.class, () -> + generator.generateValueOfType(NotGeneratable.class) + ); + } + + private int calculateTreeDepth(BinaryTreeNode node) { + if (node == null) { + return 0; + } + return Math.max( + calculateTreeDepth(node.getLeft()), + calculateTreeDepth(node.getRight()) + ) + 1; + } + + private void printTree(BinaryTreeNode node, String prefix, boolean isTail) { + if (node == null) { + return; + } + + System.out.println(prefix + (isTail ? "└── " : "├── ") + node.getData()); + + if (node.getLeft() != null || node.getRight() != null) { + if (node.getLeft() != null) { + printTree(node.getLeft(), prefix + (isTail ? " " : "│ "), node.getRight() == null); + } + if (node.getRight() != null) { + printTree(node.getRight(), prefix + (isTail ? " " : "│ "), true); + } + } + } +} + From cb540d6af105f218a80ddda4297343d32213a87c Mon Sep 17 00:00:00 2001 From: Karim Khasan Date: Tue, 4 Nov 2025 20:06:54 +0300 Subject: [PATCH 3/8] fix: fix for generic type erasure --- .../java/org/example/classes/CartStorage.java | 31 ++++++++ .../generator/AbstractValueGenerator.java | 3 +- .../java/org/example/generator/Generator.java | 71 ++++++++++--------- .../generator/PrimitiveValueGenerator.java | 8 +-- .../org/example/generator/ValueGenerator.java | 4 +- .../org/example/generator/GeneratorTest.java | 41 ++++++++++- 6 files changed, 113 insertions(+), 45 deletions(-) create mode 100644 src/main/java/org/example/classes/CartStorage.java diff --git a/src/main/java/org/example/classes/CartStorage.java b/src/main/java/org/example/classes/CartStorage.java new file mode 100644 index 0000000..a562232 --- /dev/null +++ b/src/main/java/org/example/classes/CartStorage.java @@ -0,0 +1,31 @@ +package org.example.classes; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.example.annotation.Generatable; + +@Generatable +public class CartStorage { + private final Map> buyerCarts; + + public CartStorage(Map> buyerCarts) { + this.buyerCarts = buyerCarts; + } + + public Map> getBuyerCarts() { + return buyerCarts; + } + + public void addBuyerCart(String buyer, Cart cart) { + buyerCarts.computeIfAbsent(buyer, k -> new ArrayList<>()).add(cart); + } + + @Override + public String toString() { + return "CartStorage{" + + "buyerCarts=" + buyerCarts + + '}'; + } +} diff --git a/src/main/java/org/example/generator/AbstractValueGenerator.java b/src/main/java/org/example/generator/AbstractValueGenerator.java index 28d15af..751e015 100644 --- a/src/main/java/org/example/generator/AbstractValueGenerator.java +++ b/src/main/java/org/example/generator/AbstractValueGenerator.java @@ -1,6 +1,5 @@ package org.example.generator; -import java.lang.reflect.Type; import java.util.Map; import java.util.Random; import java.util.function.Function; @@ -18,7 +17,7 @@ public boolean supports(Class clazz) { } @Override - public Object generate(Class clazz, Type genericType) { + public Object generate(Class clazz) { return getMappingFunction().get(clazz).apply(random); } diff --git a/src/main/java/org/example/generator/Generator.java b/src/main/java/org/example/generator/Generator.java index 36f6a26..1b8980e 100644 --- a/src/main/java/org/example/generator/Generator.java +++ b/src/main/java/org/example/generator/Generator.java @@ -5,13 +5,16 @@ import java.lang.reflect.Parameter; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; +import java.util.ArrayDeque; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Random; import java.util.Set; +import java.util.function.Supplier; import org.example.annotation.Generatable; @@ -40,18 +43,15 @@ public Object generateValueOfType(Class clazz) { private Object generateValueOfTypeInternal(Class clazz, Type genericType, int depth) throws Exception { var valueGenerator = findValueGeneratorForClass(clazz); if (valueGenerator != null) { - return valueGenerator.generate(clazz, genericType); + return valueGenerator.generate(clazz); } if (depth > MAX_RECURSION_DEPTH) { return null; } - if (List.class.isAssignableFrom(clazz)) { - return generateList(genericType, depth); - } - if (Set.class.isAssignableFrom(clazz)) { - return generateSet(genericType, depth); + if (Collection.class.isAssignableFrom(clazz)) { + return generateCollection(clazz, genericType, depth); } if (Map.class.isAssignableFrom(clazz)) { return generateMap(genericType, depth); @@ -77,61 +77,64 @@ private ValueGenerator findValueGeneratorForClass(Class clazz) { .orElse(null); } - private List generateList(Type genericType, int depth) throws Exception { - Class elementType = getCollectionElementType(genericType); - - int size = random.nextInt(MAX_COLLECTION_SIZE + 1); - List list = new ArrayList<>(); - for (int i = 0; i < size; i++) { - Object element = generateValueOfTypeInternal(elementType, null, depth); - list.add(element); - } - return list; - } - - private Set generateSet(Type genericType, int depth) throws Exception { - Class elementType = getCollectionElementType(genericType); - - int size = random.nextInt(MAX_COLLECTION_SIZE + 1); - Set set = new HashSet<>(); - for (int i = 0; i < size; i++) { - Object element = generateValueOfTypeInternal(elementType, null, depth); - set.add(element); + private Collection generateCollection(Class clazz, Type genericType, int depth) throws Exception { + if (List.class.isAssignableFrom(clazz)) { + return generateCollectionInternal(genericType, depth, ArrayList::new); + } else if (Set.class.isAssignableFrom(clazz)) { + return generateCollectionInternal(genericType, depth, HashSet::new); + } else { + return generateCollectionInternal(genericType, depth, ArrayDeque::new); } - return set; } - private Class getCollectionElementType(Type genericType) { + private Collection generateCollectionInternal( + Type genericType, + int depth, + Supplier> collectionSupplier + ) throws Exception { Class elementType = Object.class; + Type elementGenericType = null; if (genericType instanceof ParameterizedType paramType) { Type[] typeArgs = paramType.getActualTypeArguments(); if (typeArgs.length > 0) { - elementType = getClassFromType(typeArgs[0]); + elementGenericType = typeArgs[0]; + elementType = getClassFromType(elementGenericType); } } - return elementType; + + int size = random.nextInt(MAX_COLLECTION_SIZE + 1); + var collection = collectionSupplier.get(); + for (int i = 0; i < size; i++) { + Object element = generateValueOfTypeInternal(elementType, elementGenericType, depth); + collection.add(element); + } + return collection; } private Map generateMap(Type genericType, int depth) throws Exception { Class keyType = Object.class; + Type keyGenericType = null; Class valueType = Object.class; + Type valueGenericType = null; if (genericType instanceof ParameterizedType paramType) { Type[] typeArgs = paramType.getActualTypeArguments(); if (typeArgs.length > 0) { - keyType = getClassFromType(typeArgs[0]); + keyGenericType = typeArgs[0]; + keyType = getClassFromType(keyGenericType); } if (typeArgs.length > 1) { - valueType = getClassFromType(typeArgs[1]); + valueGenericType = typeArgs[1]; + valueType = getClassFromType(valueGenericType); } } int size = random.nextInt(MAX_COLLECTION_SIZE + 1); Map map = new HashMap<>(); for (int i = 0; i < size; i++) { - Object key = generateValueOfTypeInternal(keyType, null, depth); - Object value = generateValueOfTypeInternal(valueType, null, depth); + Object key = generateValueOfTypeInternal(keyType, keyGenericType, depth); + Object value = generateValueOfTypeInternal(valueType, valueGenericType, depth); map.put(key, value); } return map; diff --git a/src/main/java/org/example/generator/PrimitiveValueGenerator.java b/src/main/java/org/example/generator/PrimitiveValueGenerator.java index 5e316f0..e95d201 100644 --- a/src/main/java/org/example/generator/PrimitiveValueGenerator.java +++ b/src/main/java/org/example/generator/PrimitiveValueGenerator.java @@ -14,10 +14,10 @@ public class PrimitiveValueGenerator extends AbstractValueGenerator { entry(Integer.class, Random::nextInt), entry(long.class, Random::nextLong), entry(Long.class, Random::nextLong), - entry(double.class, (random) -> random.nextDouble() * 100), - entry(Double.class, (random) -> random.nextDouble() * 100), - entry(float.class, (random) -> random.nextFloat() * 100), - entry(Float.class, (random) -> random.nextFloat() * 100), + entry(double.class, (random) -> random.nextDouble() * 1000), + entry(Double.class, (random) -> random.nextDouble() * 1000), + entry(float.class, (random) -> random.nextFloat() * 1000), + entry(Float.class, (random) -> random.nextFloat() * 1000), entry(short.class, (random) -> (short) random.nextInt(Short.MAX_VALUE)), entry(Short.class, (random) -> (short) random.nextInt(Short.MAX_VALUE)), entry(byte.class, (random) -> (byte) random.nextInt(Byte.MAX_VALUE)), diff --git a/src/main/java/org/example/generator/ValueGenerator.java b/src/main/java/org/example/generator/ValueGenerator.java index e0586c4..21da2b5 100644 --- a/src/main/java/org/example/generator/ValueGenerator.java +++ b/src/main/java/org/example/generator/ValueGenerator.java @@ -1,8 +1,6 @@ package org.example.generator; -import java.lang.reflect.Type; - public interface ValueGenerator { boolean supports(Class clazz); - Object generate(Class clazz, Type genericType); + Object generate(Class clazz); } diff --git a/src/test/java/org/example/generator/GeneratorTest.java b/src/test/java/org/example/generator/GeneratorTest.java index de1d97e..8d6d64e 100644 --- a/src/test/java/org/example/generator/GeneratorTest.java +++ b/src/test/java/org/example/generator/GeneratorTest.java @@ -5,6 +5,7 @@ import org.example.classes.BinaryTreeNode; import org.example.classes.Cart; +import org.example.classes.CartStorage; import org.example.classes.Example; import org.example.classes.Product; import org.example.classes.Rectangle; @@ -92,8 +93,44 @@ void generateCart() { assertNotNull(item.getName()); }); - System.out.println("Generated Cart with " + cart.getItems().size() + " items:"); - cart.getItems().forEach(p -> System.out.println(" - " + p)); + System.out.println("Generated Cart: " + cart); + } + + @Test + void generateCartStorage() { + Object result = generator.generateValueOfType(CartStorage.class); + + assertNotNull(result); + assertInstanceOf(CartStorage.class, result); + + CartStorage cartStorage = (CartStorage) result; + assertNotNull(cartStorage.getBuyerCarts(), "Buyer carts map should not be null"); + assertTrue( + cartStorage.getBuyerCarts().size() <= MAX_COLLECTION_SIZE, + "CartStorage should have at most " + MAX_COLLECTION_SIZE + " buyers" + ); + + cartStorage.getBuyerCarts().forEach((buyer, carts) -> { + assertNotNull(buyer, "Buyer name (key) should not be null"); + assertInstanceOf(String.class, buyer, "Key should be String"); + + assertNotNull(carts, "Cart list should not be null"); + assertInstanceOf(List.class, carts, "Value should be List"); + assertTrue(carts.size() <= MAX_COLLECTION_SIZE, "Cart list should be limited"); + + carts.forEach(cart -> { + assertInstanceOf(Cart.class, cart, "Element should be Cart"); + assertNotNull(cart.getItems(), "Cart items should not be null"); + assertTrue(cart.getItems().size() <= MAX_COLLECTION_SIZE, "Cart items should be limited"); + + cart.getItems().forEach(product -> { + assertInstanceOf(Product.class, product); + assertNotNull(product.getName()); + }); + }); + }); + + System.out.println("Generated CartStorage: " + cartStorage); } @Test From d8dd9c5b087d80cba601cbc4b303c91a7b18e2c9 Mon Sep 17 00:00:00 2001 From: Karim Khasan Date: Tue, 4 Nov 2025 20:09:47 +0300 Subject: [PATCH 4/8] chore: add build workflow --- .github/workflows/java.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/java.yml diff --git a/.github/workflows/java.yml b/.github/workflows/java.yml new file mode 100644 index 0000000..1180419 --- /dev/null +++ b/.github/workflows/java.yml @@ -0,0 +1,25 @@ +name: Build Java + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v5 + - name: Set up JDK 21 + uses: actions/setup-java@v5 + with: + java-version: '21' + distribution: 'temurin' + - name: Validate Gradle wrapper + uses: gradle/actions/wrapper-validation@v5 + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v5 + - name: Build with Gradle + run: ./gradlew build From 10f4552af7dc0f534ebfe406cda0cb04e776f2cb Mon Sep 17 00:00:00 2001 From: Karim Khasan Date: Tue, 4 Nov 2025 20:13:51 +0300 Subject: [PATCH 5/8] chore: add debugging to workflow --- .github/workflows/java.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/java.yml b/.github/workflows/java.yml index 1180419..e1a865c 100644 --- a/.github/workflows/java.yml +++ b/.github/workflows/java.yml @@ -22,4 +22,4 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v5 - name: Build with Gradle - run: ./gradlew build + run: ./gradlew build --debug From 36ddcfabac1c1044c8a0c1934136b66131b65451 Mon Sep 17 00:00:00 2001 From: Karim Khasan Date: Tue, 4 Nov 2025 20:16:53 +0300 Subject: [PATCH 6/8] test: shape sides are not necessarily positive --- .github/workflows/java.yml | 2 +- src/test/java/org/example/generator/GeneratorTest.java | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/java.yml b/.github/workflows/java.yml index e1a865c..1180419 100644 --- a/.github/workflows/java.yml +++ b/.github/workflows/java.yml @@ -22,4 +22,4 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v5 - name: Build with Gradle - run: ./gradlew build --debug + run: ./gradlew build diff --git a/src/test/java/org/example/generator/GeneratorTest.java b/src/test/java/org/example/generator/GeneratorTest.java index 8d6d64e..b83ce36 100644 --- a/src/test/java/org/example/generator/GeneratorTest.java +++ b/src/test/java/org/example/generator/GeneratorTest.java @@ -143,8 +143,6 @@ void generateShapeImplementation() { "Shape should be either Rectangle or Triangle"); Shape shape = (Shape) result; - assertTrue(shape.getArea() >= 0); - assertTrue(shape.getPerimeter() >= 0); System.out.println("Generated Shape: " + shape.getClass().getSimpleName() + " = " + shape); } From c503ec90cec413ac2e3f7c0bc8dc990a72d0c6d3 Mon Sep 17 00:00:00 2001 From: Karim Khasan Date: Tue, 4 Nov 2025 20:20:50 +0300 Subject: [PATCH 7/8] style: add blank lines --- .../javax.annotation.processing.Processor | 2 +- processor/build.gradle.kts | 21 ------------------- src/main/java/org/example/classes/Cart.java | 2 +- .../java/org/example/classes/Rectangle.java | 2 +- src/main/java/org/example/classes/Shape.java | 2 +- .../java/org/example/classes/Triangle.java | 2 +- 6 files changed, 5 insertions(+), 26 deletions(-) delete mode 100644 processor/build.gradle.kts diff --git a/annotation-processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/annotation-processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor index e468d11..e53d0ec 100644 --- a/annotation-processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor +++ b/annotation-processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor @@ -1 +1 @@ -org.example.annotation.processor.GeneratableProcessor \ No newline at end of file +org.example.annotation.processor.GeneratableProcessor diff --git a/processor/build.gradle.kts b/processor/build.gradle.kts deleted file mode 100644 index 22447dd..0000000 --- a/processor/build.gradle.kts +++ /dev/null @@ -1,21 +0,0 @@ -plugins { - id("java") -} - -group = "org.example" -version = "1.0-SNAPSHOT" - -repositories { - mavenCentral() -} - -dependencies { - compileOnly(":reflection-template") - - testImplementation(platform("org.junit:junit-bom:5.10.0")) - testImplementation("org.junit.jupiter:junit-jupiter") -} - -tasks.test { - useJUnitPlatform() -} \ No newline at end of file diff --git a/src/main/java/org/example/classes/Cart.java b/src/main/java/org/example/classes/Cart.java index 1238937..1994482 100644 --- a/src/main/java/org/example/classes/Cart.java +++ b/src/main/java/org/example/classes/Cart.java @@ -28,4 +28,4 @@ public String toString() { "items=" + items + '}'; } -} \ No newline at end of file +} diff --git a/src/main/java/org/example/classes/Rectangle.java b/src/main/java/org/example/classes/Rectangle.java index a57e51a..8346da6 100644 --- a/src/main/java/org/example/classes/Rectangle.java +++ b/src/main/java/org/example/classes/Rectangle.java @@ -29,4 +29,4 @@ public String toString() { ", width=" + width + '}'; } -} \ No newline at end of file +} diff --git a/src/main/java/org/example/classes/Shape.java b/src/main/java/org/example/classes/Shape.java index c20a851..e105048 100644 --- a/src/main/java/org/example/classes/Shape.java +++ b/src/main/java/org/example/classes/Shape.java @@ -3,4 +3,4 @@ public interface Shape { double getArea(); double getPerimeter(); -} \ No newline at end of file +} diff --git a/src/main/java/org/example/classes/Triangle.java b/src/main/java/org/example/classes/Triangle.java index 67cba9a..62e316c 100644 --- a/src/main/java/org/example/classes/Triangle.java +++ b/src/main/java/org/example/classes/Triangle.java @@ -33,4 +33,4 @@ public String toString() { ", sideC=" + sideC + '}'; } -} \ No newline at end of file +} From 48d540542e07cf95b45793843b424370496487ad Mon Sep 17 00:00:00 2001 From: Karim Khasan Date: Tue, 4 Nov 2025 20:31:30 +0300 Subject: [PATCH 8/8] docs: add generatable processor comment --- .../example/annotation/processor/GeneratableProcessor.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/annotation-processor/src/main/java/org/example/annotation/processor/GeneratableProcessor.java b/annotation-processor/src/main/java/org/example/annotation/processor/GeneratableProcessor.java index 9385f41..586da10 100644 --- a/annotation-processor/src/main/java/org/example/annotation/processor/GeneratableProcessor.java +++ b/annotation-processor/src/main/java/org/example/annotation/processor/GeneratableProcessor.java @@ -50,6 +50,10 @@ private TypeElement getTypeElement(Element element) { return (element.getKind() == ElementKind.CLASS) ? (TypeElement) element : null; } + /** + * При процессинге аннотации {@link Generatable} "кэшируем" классы, которые её используют, + * для дальнейшего поиска при генерации производных классов, реализующих интерфейсы + */ private void generateGeneratableRegistry(List typeElements) { try { JavaFileObject file = processingEnv.getFiler()