From 5f2488b83d0e6f6c87ac94fcdd789c98442c56c4 Mon Sep 17 00:00:00 2001 From: "github-classroom[bot]" <66690702+github-classroom[bot]@users.noreply.github.com> Date: Sat, 1 Nov 2025 15:32:41 +0000 Subject: [PATCH 1/6] 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 20beff5adb07f07aa1617f125c0f7b4ca751f978 Mon Sep 17 00:00:00 2001 From: Vlad Denisov Date: Mon, 3 Nov 2025 14:54:40 +0300 Subject: [PATCH 2/6] feat: add solution --- .../org/example/classes/BinaryTreeNode.java | 3 + src/main/java/org/example/classes/Cart.java | 3 + .../java/org/example/classes/Example.java | 3 + .../java/org/example/classes/Product.java | 3 + .../java/org/example/classes/Rectangle.java | 3 + src/main/java/org/example/classes/Shape.java | 3 + .../java/org/example/classes/Triangle.java | 3 + .../org/example/generator/Generatable.java | 13 + .../java/org/example/generator/Generator.java | 445 +++++++++++++++++- .../org/example/generator/GeneratorTest.java | 68 +++ 10 files changed, 543 insertions(+), 4 deletions(-) create mode 100644 src/main/java/org/example/generator/Generatable.java create mode 100644 src/test/java/org/example/generator/GeneratorTest.java diff --git a/src/main/java/org/example/classes/BinaryTreeNode.java b/src/main/java/org/example/classes/BinaryTreeNode.java index 046ff56..52a9157 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.generator.Generatable; + +@Generatable public class BinaryTreeNode { private Integer data; private BinaryTreeNode left; diff --git a/src/main/java/org/example/classes/Cart.java b/src/main/java/org/example/classes/Cart.java index 965237d..bd34e2b 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.generator.Generatable; + +@Generatable public class Cart { private List items; diff --git a/src/main/java/org/example/classes/Example.java b/src/main/java/org/example/classes/Example.java index eac9463..e0c8d1c 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.generator.Generatable; + +@Generatable public class Example { int i; diff --git a/src/main/java/org/example/classes/Product.java b/src/main/java/org/example/classes/Product.java index e7dcc89..ba7aa89 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.generator.Generatable; + +@Generatable public class Product { private String name; private double price; diff --git a/src/main/java/org/example/classes/Rectangle.java b/src/main/java/org/example/classes/Rectangle.java index 90b0886..5f6b174 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.generator.Generatable; + +@Generatable public class Rectangle implements Shape { private double length; private double width; diff --git a/src/main/java/org/example/classes/Shape.java b/src/main/java/org/example/classes/Shape.java index c20a851..2c9c3ef 100644 --- a/src/main/java/org/example/classes/Shape.java +++ b/src/main/java/org/example/classes/Shape.java @@ -1,5 +1,8 @@ package org.example.classes; +import org.example.generator.Generatable; + +@Generatable({Rectangle.class, Triangle.class}) public interface Shape { double getArea(); double getPerimeter(); diff --git a/src/main/java/org/example/classes/Triangle.java b/src/main/java/org/example/classes/Triangle.java index 011e96f..e2c4c5a 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.generator.Generatable; + +@Generatable public class Triangle implements Shape { private double sideA; private double sideB; diff --git a/src/main/java/org/example/generator/Generatable.java b/src/main/java/org/example/generator/Generatable.java new file mode 100644 index 0000000..3737fab --- /dev/null +++ b/src/main/java/org/example/generator/Generatable.java @@ -0,0 +1,13 @@ +package org.example.generator; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Generatable { + Class[] value() default {}; +} + diff --git a/src/main/java/org/example/generator/Generator.java b/src/main/java/org/example/generator/Generator.java index 9d86bfb..57f7500 100644 --- a/src/main/java/org/example/generator/Generator.java +++ b/src/main/java/org/example/generator/Generator.java @@ -1,18 +1,455 @@ package org.example.generator; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Array; import java.lang.reflect.Constructor; +import java.lang.reflect.Field; 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.net.URISyntaxException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Queue; import java.util.Random; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; public class Generator { + private static final int DEFAULT_MAX_DEPTH = 3; + private static final int DEFAULT_MAX_COLLECTION_SIZE = 3; + private static final String SCAN_PACKAGE = "org.example.classes"; + + private static final ConcurrentHashMap, List>> IMPLEMENTATIONS_CACHE = new ConcurrentHashMap<>(); + + private final Random random; + private final int maxDepth; + private final int maxCollectionSize; + + public Generator() { + this(new Random(), DEFAULT_MAX_DEPTH, DEFAULT_MAX_COLLECTION_SIZE); + } + + public Generator(Random random, int maxDepth, int maxCollectionSize) { + this.random = Objects.requireNonNull(random, "random"); + this.maxDepth = Math.max(1, maxDepth); + this.maxCollectionSize = Math.max(0, maxCollectionSize); + } + public Object generateValueOfType(Class clazz) throws InvocationTargetException, InstantiationException, IllegalAccessException { - Constructor[] constructors = clazz.getDeclaredConstructors(); + return generateValueOfType(clazz, 0); + } + + private Object generateValueOfType(Class clazz, int depth) throws InvocationTargetException, InstantiationException, IllegalAccessException { + if (clazz == null) { + return null; + } - int randomConstructorIndex = new Random().nextInt(constructors.length); - Constructor randomConstructor = constructors[randomConstructorIndex]; - return randomConstructor.newInstance(111); + Object simpleValue = tryGenerateSimpleValue(clazz); + if (simpleValue != null) { + return simpleValue; + } + + if (depth >= maxDepth) { + return null; + } + + Class concreteClass = resolveConcreteClass(clazz); + Object instance = instantiate(concreteClass, depth); + populateFields(instance, depth + 1); + return instance; } + private Object tryGenerateSimpleValue(Class clazz) { + if (clazz.isPrimitive()) { + return generatePrimitiveValue(clazz); + } + + Object basicValue = generateWrapperOrCommon(clazz); + if (basicValue != null) { + return basicValue; + } + + if (clazz.isEnum()) { + Object[] constants = clazz.getEnumConstants(); + if (constants.length == 0) { + throw new IllegalArgumentException("Cannot instantiate enum without constants: " + clazz.getName()); + } + return constants[random.nextInt(constants.length)]; + } + + return null; + } + + private Class resolveConcreteClass(Class clazz) { + if (clazz.isInterface() || Modifier.isAbstract(clazz.getModifiers())) { + List> candidates = findImplementations(clazz); + if (candidates.isEmpty()) { + throw new IllegalArgumentException("No generatable implementations found for type: " + clazz.getName()); + } + return candidates.get(random.nextInt(candidates.size())); + } + + if (clazz.getAnnotation(Generatable.class) == null) { + throw new IllegalArgumentException("Type is not marked as generatable: " + clazz.getName()); + } + + return clazz; + } + + private Object instantiate(Class clazz, int depth) throws InvocationTargetException, InstantiationException, IllegalAccessException { + List> constructors = new ArrayList<>(Arrays.asList(clazz.getDeclaredConstructors())); + if (constructors.isEmpty()) { + throw new IllegalArgumentException("Type has no accessible constructors: " + clazz.getName()); + } + Collections.shuffle(constructors, random); + + for (Constructor constructor : constructors) { + Parameter[] parameters = constructor.getParameters(); + Object[] args = new Object[parameters.length]; + boolean success = true; + + for (int i = 0; i < parameters.length; i++) { + try { + args[i] = generateParameterValue(parameters[i], depth + 1); + } catch (IllegalArgumentException e) { + success = false; + break; + } + } + + if (success) { + return constructor.newInstance(args); + } + } + + throw new IllegalArgumentException("Unable to instantiate type: " + clazz.getName()); + } + + private Object generateParameterValue(Parameter parameter, int depth) throws InvocationTargetException, InstantiationException, IllegalAccessException { + Class rawType = parameter.getType(); + + if (rawType.isPrimitive()) { + return generatePrimitiveValue(rawType); + } + + Object basicValue = generateWrapperOrCommon(rawType); + if (basicValue != null) { + return basicValue; + } + + // For constructor parameters: Collections/Maps/Arrays should be empty + if (Collection.class.isAssignableFrom(rawType)) { + return createEmptyCollection(rawType); + } + if (Map.class.isAssignableFrom(rawType)) { + return createEmptyMap(rawType); + } + if (rawType.isArray()) { + return Array.newInstance(rawType.getComponentType(), 0); + } + + if (depth >= maxDepth) { + return null; + } + + return generateValueOfType(rawType, depth); + } + + private Collection generateCollection(Type genericType, Class rawType, int depth) throws InvocationTargetException, InstantiationException, IllegalAccessException { + int size = maxCollectionSize == 0 ? 0 : random.nextInt(maxCollectionSize + 1); + Collection collection; + + if (rawType.isInterface()) { + if (List.class.isAssignableFrom(rawType)) { + collection = new ArrayList<>(); + } else if (Set.class.isAssignableFrom(rawType)) { + collection = new HashSet<>(); + } else if (Queue.class.isAssignableFrom(rawType)) { + collection = new LinkedList<>(); + } else { + throw new IllegalArgumentException("Unsupported collection interface: " + rawType.getName()); + } + } else { + try { + Constructor ctor = rawType.getDeclaredConstructor(); + collection = (Collection) ctor.newInstance(); + } catch (NoSuchMethodException e) { + throw new IllegalArgumentException("Collection type requires no-arg constructor: " + rawType.getName(), e); + } + } + + Class elementClass = resolveCollectionElementType(genericType); + for (int i = 0; i < size; i++) { + collection.add(generateCollectionElement(elementClass, depth + 1)); + } + return collection; + } + + private Map generateMap(Type genericType, Class rawType, int depth) throws InvocationTargetException, InstantiationException, IllegalAccessException { + int size = 1 + random.nextInt(Math.max(1, Math.min(2, maxCollectionSize))); + Map map; + + if (rawType.isInterface()) { + map = new HashMap<>(); + } else { + try { + Constructor ctor = rawType.getDeclaredConstructor(); + map = (Map) ctor.newInstance(); + } catch (NoSuchMethodException e) { + throw new IllegalArgumentException("Map type requires no-arg constructor: " + rawType.getName(), e); + } + } + + Class[] kv = resolveMapKeyValueTypes(genericType); + Class keyClass = kv[0]; + Class valueClass = kv[1]; + for (int i = 0; i < size; i++) { + Object key = isImmutableKeyType(keyClass) ? generateElementForType(keyClass, depth + 1) : null; + if (key == null) continue; + Object value = generateElementForType(valueClass, depth + 1); + map.put(key, value); + } + return map; + } + + private Object generateCollectionElement(Class elementClass, int depth) throws InvocationTargetException, InstantiationException, IllegalAccessException { + if (elementClass == Object.class) { + return null; + } + if (elementClass.isPrimitive()) { + return generatePrimitiveValue(elementClass); + } + Object basicValue = generateWrapperOrCommon(elementClass); + if (basicValue != null) { + return basicValue; + } + if (depth >= maxDepth) { + return null; + } + return generateValueOfType(elementClass, depth); + } + + private Class resolveCollectionElementType(Type genericType) { + if (genericType instanceof ParameterizedType parameterizedType) { + Type[] arguments = parameterizedType.getActualTypeArguments(); + if (arguments.length == 1) { + Type argument = arguments[0]; + if (argument instanceof Class aClass) { + return aClass; + } + if (argument instanceof ParameterizedType parameterizedArgument) { + Type raw = parameterizedArgument.getRawType(); + if (raw instanceof Class aClass) { + return aClass; + } + } + } + } + return Object.class; + } + + private Class[] resolveMapKeyValueTypes(Type genericType) { + Class key = Object.class; + Class value = Object.class; + if (genericType instanceof ParameterizedType parameterizedType) { + Type[] args = parameterizedType.getActualTypeArguments(); + if (args.length == 2) { + if (args[0] instanceof Class k) key = k; + if (args[1] instanceof Class v) value = v; + } + } + return new Class[]{key, value}; + } + + private Object generateWrapperOrCommon(Class cl) { + if (cl == String.class) { + return randomString(); + } + if (cl == Integer.class) { + return random.nextInt(201) - 100; + } + if (cl == Long.class) { + return (long) (random.nextInt(2001) - 1000); + } + if (cl == Double.class) { + return (random.nextDouble() * 200.0) - 100.0; + } + if (cl == Float.class) { + return (random.nextFloat() * 200.0f) - 100.0f; + } + if (cl == Short.class) { + return (short) (random.nextInt(2001) - 1000); + } + if (cl == Byte.class) { + return (byte) (random.nextInt(201) - 100); + } + if (cl == Boolean.class) { + return random.nextBoolean(); + } + if (cl == Character.class) { + return (char) (random.nextInt(26) + 'a'); + } + return null; + } + + private Object generatePrimitiveValue(Class cl) { + if (cl == int.class) { + return random.nextInt(201) - 100; + } + if (cl == long.class) { + return (long) (random.nextInt(2001) - 1000); + } + if (cl == double.class) { + return (random.nextDouble() * 200.0) - 100.0; + } + if (cl == float.class) { + return (random.nextFloat() * 200.0f) - 100.0f; + } + if (cl == short.class) { + return (short) (random.nextInt(2001) - 1000); + } + if (cl == byte.class) { + return (byte) (random.nextInt(201) - 100); + } + if (cl == boolean.class) { + return random.nextBoolean(); + } + if (cl == char.class) { + return (char) (random.nextInt(26) + 'a'); + } + throw new IllegalArgumentException("Unsupported primitive type: " + cl.getName()); + } + + private String randomString() { + int length = random.nextInt(10) + 1; + StringBuilder builder = new StringBuilder(length); + for (int i = 0; i < length; i++) { + builder.append((char) ('a' + random.nextInt(26))); + } + return builder.toString(); + } + + private List> findImplementations(Class targetType) { + return IMPLEMENTATIONS_CACHE.computeIfAbsent(targetType, t -> { + List> found = new ArrayList<>(); + String path = SCAN_PACKAGE.replace('.', '/'); + try { + var resources = Thread.currentThread().getContextClassLoader().getResources(path); + while (resources.hasMoreElements()) { + URL url = resources.nextElement(); + if ("file".equals(url.getProtocol())) { + File dir = new File(url.toURI()); + File[] files = dir.listFiles((d, name) -> name.endsWith(".class") && !name.contains("$")); + if (files == null) continue; + for (File f : files) { + String className = SCAN_PACKAGE + "." + f.getName().substring(0, f.getName().length() - 6); + try { + Class candidate = Class.forName(className); + if (!Modifier.isAbstract(candidate.getModifiers()) + && candidate.getAnnotation(Generatable.class) != null + && t.isAssignableFrom(candidate)) { + found.add(candidate); + } + } catch (ClassNotFoundException ignored) { + } + } + } + } + } catch (IOException | URISyntaxException ignored) { + } + return found; + }); + } + + private void populateFields(Object instance, int depth) throws InvocationTargetException, InstantiationException, IllegalAccessException { + if (instance == null || depth > maxDepth) return; + + Class type = instance.getClass(); + for (Class c = type; c != null && c != Object.class; c = c.getSuperclass()) { + Field[] fields = c.getDeclaredFields(); + for (Field field : fields) { + int mod = field.getModifiers(); + if (Modifier.isStatic(mod) || Modifier.isFinal(mod)) continue; + + field.setAccessible(true); + Class fType = field.getType(); + Type gType = field.getGenericType(); + + Object value; + if (fType.isArray()) { + Class component = fType.getComponentType(); + int size = 1 + random.nextInt(2); + Object array = Array.newInstance(component, size); + for (int i = 0; i < size; i++) { + Array.set(array, i, generateElementForType(component, depth)); + } + value = array; + } else if (Collection.class.isAssignableFrom(fType)) { + Collection coll = generateCollection(gType, fType, depth); + value = coll; + } else if (Map.class.isAssignableFrom(fType)) { + Map m = generateMap(gType, fType, depth); + value = m; + } else { + Object simple = tryGenerateSimpleValue(fType); + if (simple != null) { + value = simple; + } else if (depth < maxDepth) { + value = generateValueOfType(fType, depth); + } else { + value = null; + } + } + + try { + field.set(instance, value); + } catch (IllegalAccessException ignored) { + } + } + } + } + + private static final Set> IMMUTABLE_KEY_TYPES = Set.of(String.class, Integer.class, Long.class, Double.class, Float.class, Short.class, Byte.class, Character.class); + + private boolean isImmutableKeyType(Class cl) { + if (cl == null) return false; + if (cl.isEnum()) return true; + return IMMUTABLE_KEY_TYPES.contains(cl) || cl.isPrimitive(); + } + + private Object generateElementForType(Class elementClass, int depth) throws InvocationTargetException, InstantiationException, IllegalAccessException { + if (elementClass == Object.class) return null; + Object simple = tryGenerateSimpleValue(elementClass); + if (simple != null) return simple; + if (depth >= maxDepth) return null; + return generateValueOfType(elementClass, depth); + } + + private Collection createEmptyCollection(Class rawType) { + if (List.class.isAssignableFrom(rawType)) return new ArrayList<>(); + if (Set.class.isAssignableFrom(rawType)) return new HashSet<>(); + if (Queue.class.isAssignableFrom(rawType)) return new LinkedList<>(); + if (Collection.class.isAssignableFrom(rawType)) return new ArrayList<>(); + throw new IllegalArgumentException("Unsupported collection type: " + rawType.getName()); + } + + private Map createEmptyMap(Class rawType) { + if (Map.class.isAssignableFrom(rawType)) return new HashMap<>(); + throw new IllegalArgumentException("Unsupported map type: " + rawType.getName()); + } } 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..067b29a --- /dev/null +++ b/src/test/java/org/example/generator/GeneratorTest.java @@ -0,0 +1,68 @@ +package org.example.generator; + +import org.example.classes.BinaryTreeNode; +import org.example.classes.Cart; +import org.example.classes.Product; +import org.example.classes.Shape; +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.InvocationTargetException; +import java.util.List; +import java.util.Random; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class GeneratorTest { + + private final Generator generator = new Generator(new Random(42), 3, 3); + + @Test + void generatesExampleInstance() { + assertDoesNotThrow(() -> { + Object value = generator.generateValueOfType(org.example.classes.Example.class); + assertNotNull(value); + assertTrue(value instanceof org.example.classes.Example); + }); + } + + @RepeatedTest(5) + void generatesShapeImplementation() throws InvocationTargetException, InstantiationException, IllegalAccessException { + Shape value = (Shape) generator.generateValueOfType(Shape.class); + assertNotNull(value); + System.out.println(value.getArea()); + assertTrue(value instanceof Shape); + } + + @Test + void generatesCartWithProducts() throws InvocationTargetException, InstantiationException, IllegalAccessException { + Cart cart = (Cart) generator.generateValueOfType(Cart.class); + assertNotNull(cart); + List items = cart.getItems(); + assertNotNull(items); + System.out.println(items.getFirst().getPrice()); + for (Product product : items) { + assertNotNull(product); + } + } + + @Test + void respectsConfiguredMaxDepthForBinaryTree() throws InvocationTargetException, InstantiationException, IllegalAccessException { + BinaryTreeNode root = (BinaryTreeNode) generator.generateValueOfType(BinaryTreeNode.class); + assertNotNull(root); + int depth = calculateDepth(root); + System.out.println(depth); + System.out.println(root.getData()); + assertTrue(depth <= 3); + } + + private int calculateDepth(BinaryTreeNode node) { + if (node == null) { + return 0; + } + return 1 + Math.max(calculateDepth(node.getLeft()), calculateDepth(node.getRight())); + } +} + From 4b6a9207969b5f2dfd30f322ddda166c16c92e86 Mon Sep 17 00:00:00 2001 From: Vlad Denisov Date: Tue, 4 Nov 2025 22:20:06 +0300 Subject: [PATCH 3/6] Split code by files --- .../java/org/example/generator/Generator.java | 186 ++---------------- .../generator/ImplementationFinder.java | 53 +++++ .../org/example/generator/RandomValues.java | 82 ++++++++ .../org/example/generator/TypeHelpers.java | 76 +++++++ 4 files changed, 223 insertions(+), 174 deletions(-) create mode 100644 src/main/java/org/example/generator/ImplementationFinder.java create mode 100644 src/main/java/org/example/generator/RandomValues.java create mode 100644 src/main/java/org/example/generator/TypeHelpers.java diff --git a/src/main/java/org/example/generator/Generator.java b/src/main/java/org/example/generator/Generator.java index 57f7500..9389f58 100644 --- a/src/main/java/org/example/generator/Generator.java +++ b/src/main/java/org/example/generator/Generator.java @@ -1,17 +1,12 @@ package org.example.generator; -import java.io.File; -import java.io.IOException; import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Field; 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.net.URISyntaxException; -import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -25,15 +20,11 @@ import java.util.Queue; import java.util.Random; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; public class Generator { private static final int DEFAULT_MAX_DEPTH = 3; private static final int DEFAULT_MAX_COLLECTION_SIZE = 3; - private static final String SCAN_PACKAGE = "org.example.classes"; - - private static final ConcurrentHashMap, List>> IMPLEMENTATIONS_CACHE = new ConcurrentHashMap<>(); private final Random random; private final int maxDepth; @@ -75,10 +66,10 @@ private Object generateValueOfType(Class clazz, int depth) throws InvocationT private Object tryGenerateSimpleValue(Class clazz) { if (clazz.isPrimitive()) { - return generatePrimitiveValue(clazz); + return RandomValues.generatePrimitiveValue(clazz, random); } - Object basicValue = generateWrapperOrCommon(clazz); + Object basicValue = RandomValues.generateWrapperOrCommon(clazz, random); if (basicValue != null) { return basicValue; } @@ -96,7 +87,7 @@ private Object tryGenerateSimpleValue(Class clazz) { private Class resolveConcreteClass(Class clazz) { if (clazz.isInterface() || Modifier.isAbstract(clazz.getModifiers())) { - List> candidates = findImplementations(clazz); + List> candidates = ImplementationFinder.findImplementations(clazz); if (candidates.isEmpty()) { throw new IllegalArgumentException("No generatable implementations found for type: " + clazz.getName()); } @@ -143,20 +134,20 @@ private Object generateParameterValue(Parameter parameter, int depth) throws Inv Class rawType = parameter.getType(); if (rawType.isPrimitive()) { - return generatePrimitiveValue(rawType); + return RandomValues.generatePrimitiveValue(rawType, random); } - Object basicValue = generateWrapperOrCommon(rawType); + Object basicValue = RandomValues.generateWrapperOrCommon(rawType, random); if (basicValue != null) { return basicValue; } // For constructor parameters: Collections/Maps/Arrays should be empty if (Collection.class.isAssignableFrom(rawType)) { - return createEmptyCollection(rawType); + return TypeHelpers.createEmptyCollection(rawType); } if (Map.class.isAssignableFrom(rawType)) { - return createEmptyMap(rawType); + return TypeHelpers.createEmptyMap(rawType); } if (rawType.isArray()) { return Array.newInstance(rawType.getComponentType(), 0); @@ -192,7 +183,7 @@ private Collection generateCollection(Type genericType, Class rawType, int } } - Class elementClass = resolveCollectionElementType(genericType); + Class elementClass = TypeHelpers.resolveCollectionElementType(genericType); for (int i = 0; i < size; i++) { collection.add(generateCollectionElement(elementClass, depth + 1)); } @@ -215,11 +206,11 @@ private Collection generateCollection(Type genericType, Class rawType, int } } - Class[] kv = resolveMapKeyValueTypes(genericType); + Class[] kv = TypeHelpers.resolveMapKeyValueTypes(genericType); Class keyClass = kv[0]; Class valueClass = kv[1]; for (int i = 0; i < size; i++) { - Object key = isImmutableKeyType(keyClass) ? generateElementForType(keyClass, depth + 1) : null; + Object key = TypeHelpers.isImmutableKeyType(keyClass) ? generateElementForType(keyClass, depth + 1) : null; if (key == null) continue; Object value = generateElementForType(valueClass, depth + 1); map.put(key, value); @@ -232,9 +223,9 @@ private Object generateCollectionElement(Class elementClass, int depth) throw return null; } if (elementClass.isPrimitive()) { - return generatePrimitiveValue(elementClass); + return RandomValues.generatePrimitiveValue(elementClass, random); } - Object basicValue = generateWrapperOrCommon(elementClass); + Object basicValue = RandomValues.generateWrapperOrCommon(elementClass, random); if (basicValue != null) { return basicValue; } @@ -244,138 +235,6 @@ private Object generateCollectionElement(Class elementClass, int depth) throw return generateValueOfType(elementClass, depth); } - private Class resolveCollectionElementType(Type genericType) { - if (genericType instanceof ParameterizedType parameterizedType) { - Type[] arguments = parameterizedType.getActualTypeArguments(); - if (arguments.length == 1) { - Type argument = arguments[0]; - if (argument instanceof Class aClass) { - return aClass; - } - if (argument instanceof ParameterizedType parameterizedArgument) { - Type raw = parameterizedArgument.getRawType(); - if (raw instanceof Class aClass) { - return aClass; - } - } - } - } - return Object.class; - } - - private Class[] resolveMapKeyValueTypes(Type genericType) { - Class key = Object.class; - Class value = Object.class; - if (genericType instanceof ParameterizedType parameterizedType) { - Type[] args = parameterizedType.getActualTypeArguments(); - if (args.length == 2) { - if (args[0] instanceof Class k) key = k; - if (args[1] instanceof Class v) value = v; - } - } - return new Class[]{key, value}; - } - - private Object generateWrapperOrCommon(Class cl) { - if (cl == String.class) { - return randomString(); - } - if (cl == Integer.class) { - return random.nextInt(201) - 100; - } - if (cl == Long.class) { - return (long) (random.nextInt(2001) - 1000); - } - if (cl == Double.class) { - return (random.nextDouble() * 200.0) - 100.0; - } - if (cl == Float.class) { - return (random.nextFloat() * 200.0f) - 100.0f; - } - if (cl == Short.class) { - return (short) (random.nextInt(2001) - 1000); - } - if (cl == Byte.class) { - return (byte) (random.nextInt(201) - 100); - } - if (cl == Boolean.class) { - return random.nextBoolean(); - } - if (cl == Character.class) { - return (char) (random.nextInt(26) + 'a'); - } - return null; - } - - private Object generatePrimitiveValue(Class cl) { - if (cl == int.class) { - return random.nextInt(201) - 100; - } - if (cl == long.class) { - return (long) (random.nextInt(2001) - 1000); - } - if (cl == double.class) { - return (random.nextDouble() * 200.0) - 100.0; - } - if (cl == float.class) { - return (random.nextFloat() * 200.0f) - 100.0f; - } - if (cl == short.class) { - return (short) (random.nextInt(2001) - 1000); - } - if (cl == byte.class) { - return (byte) (random.nextInt(201) - 100); - } - if (cl == boolean.class) { - return random.nextBoolean(); - } - if (cl == char.class) { - return (char) (random.nextInt(26) + 'a'); - } - throw new IllegalArgumentException("Unsupported primitive type: " + cl.getName()); - } - - private String randomString() { - int length = random.nextInt(10) + 1; - StringBuilder builder = new StringBuilder(length); - for (int i = 0; i < length; i++) { - builder.append((char) ('a' + random.nextInt(26))); - } - return builder.toString(); - } - - private List> findImplementations(Class targetType) { - return IMPLEMENTATIONS_CACHE.computeIfAbsent(targetType, t -> { - List> found = new ArrayList<>(); - String path = SCAN_PACKAGE.replace('.', '/'); - try { - var resources = Thread.currentThread().getContextClassLoader().getResources(path); - while (resources.hasMoreElements()) { - URL url = resources.nextElement(); - if ("file".equals(url.getProtocol())) { - File dir = new File(url.toURI()); - File[] files = dir.listFiles((d, name) -> name.endsWith(".class") && !name.contains("$")); - if (files == null) continue; - for (File f : files) { - String className = SCAN_PACKAGE + "." + f.getName().substring(0, f.getName().length() - 6); - try { - Class candidate = Class.forName(className); - if (!Modifier.isAbstract(candidate.getModifiers()) - && candidate.getAnnotation(Generatable.class) != null - && t.isAssignableFrom(candidate)) { - found.add(candidate); - } - } catch (ClassNotFoundException ignored) { - } - } - } - } - } catch (IOException | URISyntaxException ignored) { - } - return found; - }); - } - private void populateFields(Object instance, int depth) throws InvocationTargetException, InstantiationException, IllegalAccessException { if (instance == null || depth > maxDepth) return; @@ -424,14 +283,6 @@ private void populateFields(Object instance, int depth) throws InvocationTargetE } } - private static final Set> IMMUTABLE_KEY_TYPES = Set.of(String.class, Integer.class, Long.class, Double.class, Float.class, Short.class, Byte.class, Character.class); - - private boolean isImmutableKeyType(Class cl) { - if (cl == null) return false; - if (cl.isEnum()) return true; - return IMMUTABLE_KEY_TYPES.contains(cl) || cl.isPrimitive(); - } - private Object generateElementForType(Class elementClass, int depth) throws InvocationTargetException, InstantiationException, IllegalAccessException { if (elementClass == Object.class) return null; Object simple = tryGenerateSimpleValue(elementClass); @@ -439,17 +290,4 @@ private Object generateElementForType(Class elementClass, int depth) throws I if (depth >= maxDepth) return null; return generateValueOfType(elementClass, depth); } - - private Collection createEmptyCollection(Class rawType) { - if (List.class.isAssignableFrom(rawType)) return new ArrayList<>(); - if (Set.class.isAssignableFrom(rawType)) return new HashSet<>(); - if (Queue.class.isAssignableFrom(rawType)) return new LinkedList<>(); - if (Collection.class.isAssignableFrom(rawType)) return new ArrayList<>(); - throw new IllegalArgumentException("Unsupported collection type: " + rawType.getName()); - } - - private Map createEmptyMap(Class rawType) { - if (Map.class.isAssignableFrom(rawType)) return new HashMap<>(); - throw new IllegalArgumentException("Unsupported map type: " + rawType.getName()); - } } diff --git a/src/main/java/org/example/generator/ImplementationFinder.java b/src/main/java/org/example/generator/ImplementationFinder.java new file mode 100644 index 0000000..3b62b2b --- /dev/null +++ b/src/main/java/org/example/generator/ImplementationFinder.java @@ -0,0 +1,53 @@ +package org.example.generator; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Modifier; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +public final class ImplementationFinder { + + private ImplementationFinder() {} + + private static final String SCAN_PACKAGE = "org.example.classes"; + + private static final ConcurrentHashMap, List>> IMPLEMENTATIONS_CACHE = new ConcurrentHashMap<>(); + + public static List> findImplementations(Class targetType) { + return IMPLEMENTATIONS_CACHE.computeIfAbsent(targetType, t -> { + List> found = new ArrayList<>(); + String path = SCAN_PACKAGE.replace('.', '/'); + try { + var resources = Thread.currentThread().getContextClassLoader().getResources(path); + while (resources.hasMoreElements()) { + URL url = resources.nextElement(); + if ("file".equals(url.getProtocol())) { + File dir = new File(url.toURI()); + File[] files = dir.listFiles((d, name) -> name.endsWith(".class") && !name.contains("$")); + if (files == null) continue; + for (File f : files) { + String className = SCAN_PACKAGE + "." + f.getName().substring(0, f.getName().length() - 6); + try { + Class candidate = Class.forName(className); + if (!Modifier.isAbstract(candidate.getModifiers()) + && candidate.getAnnotation(Generatable.class) != null + && t.isAssignableFrom(candidate)) { + found.add(candidate); + } + } catch (ClassNotFoundException ignored) { + } + } + } + } + } catch (IOException | URISyntaxException ignored) { + } + return found; + }); + } +} + + diff --git a/src/main/java/org/example/generator/RandomValues.java b/src/main/java/org/example/generator/RandomValues.java new file mode 100644 index 0000000..e9808fe --- /dev/null +++ b/src/main/java/org/example/generator/RandomValues.java @@ -0,0 +1,82 @@ +package org.example.generator; + +import java.util.Objects; +import java.util.Random; + +public final class RandomValues { + + private RandomValues() {} + + public static Object generateWrapperOrCommon(Class cl, Random random) { + Objects.requireNonNull(random, "random"); + if (cl == String.class) { + return randomString(random); + } + if (cl == Integer.class) { + return random.nextInt(201) - 100; + } + if (cl == Long.class) { + return (long) (random.nextInt(2001) - 1000); + } + if (cl == Double.class) { + return (random.nextDouble() * 200.0) - 100.0; + } + if (cl == Float.class) { + return (random.nextFloat() * 200.0f) - 100.0f; + } + if (cl == Short.class) { + return (short) (random.nextInt(2001) - 1000); + } + if (cl == Byte.class) { + return (byte) (random.nextInt(201) - 100); + } + if (cl == Boolean.class) { + return random.nextBoolean(); + } + if (cl == Character.class) { + return (char) (random.nextInt(26) + 'a'); + } + return null; + } + + public static Object generatePrimitiveValue(Class cl, Random random) { + Objects.requireNonNull(random, "random"); + if (cl == int.class) { + return random.nextInt(201) - 100; + } + if (cl == long.class) { + return (long) (random.nextInt(2001) - 1000); + } + if (cl == double.class) { + return (random.nextDouble() * 200.0) - 100.0; + } + if (cl == float.class) { + return (random.nextFloat() * 200.0f) - 100.0f; + } + if (cl == short.class) { + return (short) (random.nextInt(2001) - 1000); + } + if (cl == byte.class) { + return (byte) (random.nextInt(201) - 100); + } + if (cl == boolean.class) { + return random.nextBoolean(); + } + if (cl == char.class) { + return (char) (random.nextInt(26) + 'a'); + } + throw new IllegalArgumentException("Unsupported primitive type: " + cl.getName()); + } + + public static String randomString(Random random) { + Objects.requireNonNull(random, "random"); + int length = random.nextInt(10) + 1; + StringBuilder builder = new StringBuilder(length); + for (int i = 0; i < length; i++) { + builder.append((char) ('a' + random.nextInt(26))); + } + return builder.toString(); + } +} + + diff --git a/src/main/java/org/example/generator/TypeHelpers.java b/src/main/java/org/example/generator/TypeHelpers.java new file mode 100644 index 0000000..0eb6deb --- /dev/null +++ b/src/main/java/org/example/generator/TypeHelpers.java @@ -0,0 +1,76 @@ +package org.example.generator; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Set; + +public final class TypeHelpers { + + private TypeHelpers() {} + + private static final Set> IMMUTABLE_KEY_TYPES = Set.of( + String.class, Integer.class, Long.class, Double.class, + Float.class, Short.class, Byte.class, Character.class + ); + + public static Class resolveCollectionElementType(Type genericType) { + if (genericType instanceof ParameterizedType parameterizedType) { + Type[] arguments = parameterizedType.getActualTypeArguments(); + if (arguments.length == 1) { + Type argument = arguments[0]; + if (argument instanceof Class aClass) { + return aClass; + } + if (argument instanceof ParameterizedType parameterizedArgument) { + Type raw = parameterizedArgument.getRawType(); + if (raw instanceof Class aClass) { + return aClass; + } + } + } + } + return Object.class; + } + + public static Class[] resolveMapKeyValueTypes(Type genericType) { + Class key = Object.class; + Class value = Object.class; + if (genericType instanceof ParameterizedType parameterizedType) { + Type[] args = parameterizedType.getActualTypeArguments(); + if (args.length == 2) { + if (args[0] instanceof Class k) key = k; + if (args[1] instanceof Class v) value = v; + } + } + return new Class[]{key, value}; + } + + public static boolean isImmutableKeyType(Class cl) { + if (cl == null) return false; + if (cl.isEnum()) return true; + return IMMUTABLE_KEY_TYPES.contains(cl) || cl.isPrimitive(); + } + + public static Collection createEmptyCollection(Class rawType) { + if (List.class.isAssignableFrom(rawType)) return new ArrayList<>(); + if (Set.class.isAssignableFrom(rawType)) return new HashSet<>(); + if (Queue.class.isAssignableFrom(rawType)) return new LinkedList<>(); + if (Collection.class.isAssignableFrom(rawType)) return new ArrayList<>(); + throw new IllegalArgumentException("Unsupported collection type: " + rawType.getName()); + } + + public static Map createEmptyMap(Class rawType) { + if (Map.class.isAssignableFrom(rawType)) return new HashMap<>(); + throw new IllegalArgumentException("Unsupported map type: " + rawType.getName()); + } +} + + From 70f5c6061d72387b576150e5408dd659abafa869 Mon Sep 17 00:00:00 2001 From: Vlad Denisov Date: Wed, 5 Nov 2025 13:37:30 +0300 Subject: [PATCH 4/6] Remove unused params in decorator --- src/main/java/org/example/classes/Shape.java | 2 +- src/test/java/org/example/generator/GeneratorTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/example/classes/Shape.java b/src/main/java/org/example/classes/Shape.java index 2c9c3ef..3e17a1e 100644 --- a/src/main/java/org/example/classes/Shape.java +++ b/src/main/java/org/example/classes/Shape.java @@ -2,7 +2,7 @@ import org.example.generator.Generatable; -@Generatable({Rectangle.class, Triangle.class}) +@Generatable() public interface Shape { double getArea(); double getPerimeter(); diff --git a/src/test/java/org/example/generator/GeneratorTest.java b/src/test/java/org/example/generator/GeneratorTest.java index 067b29a..27b7428 100644 --- a/src/test/java/org/example/generator/GeneratorTest.java +++ b/src/test/java/org/example/generator/GeneratorTest.java @@ -28,7 +28,7 @@ void generatesExampleInstance() { }); } - @RepeatedTest(5) + @Test void generatesShapeImplementation() throws InvocationTargetException, InstantiationException, IllegalAccessException { Shape value = (Shape) generator.generateValueOfType(Shape.class); assertNotNull(value); From 4c147db1e6f173ef1485b87daf217aa71ff7cc23 Mon Sep 17 00:00:00 2001 From: Vlad Denisov Date: Wed, 5 Nov 2025 13:38:14 +0300 Subject: [PATCH 5/6] chore: remove braces --- src/main/java/org/example/classes/Shape.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/example/classes/Shape.java b/src/main/java/org/example/classes/Shape.java index 3e17a1e..7a6f41f 100644 --- a/src/main/java/org/example/classes/Shape.java +++ b/src/main/java/org/example/classes/Shape.java @@ -2,7 +2,7 @@ import org.example.generator.Generatable; -@Generatable() +@Generatable public interface Shape { double getArea(); double getPerimeter(); From e741a1a8bb6d2c3c5734c17c31c28f89ac0c7f00 Mon Sep 17 00:00:00 2001 From: Vlad Denisov Date: Wed, 5 Nov 2025 14:01:02 +0300 Subject: [PATCH 6/6] Add comments --- src/main/java/org/example/generator/Generator.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/example/generator/Generator.java b/src/main/java/org/example/generator/Generator.java index 9389f58..cf3408c 100644 --- a/src/main/java/org/example/generator/Generator.java +++ b/src/main/java/org/example/generator/Generator.java @@ -23,6 +23,7 @@ public class Generator { + // Значения по умолчанию для максимальной глубины и размера коллекций private static final int DEFAULT_MAX_DEPTH = 3; private static final int DEFAULT_MAX_COLLECTION_SIZE = 3; @@ -54,6 +55,7 @@ private Object generateValueOfType(Class clazz, int depth) throws InvocationT return simpleValue; } + // Ограничение глубины рекурсии сложных типов if (depth >= maxDepth) { return null; } @@ -142,7 +144,9 @@ private Object generateParameterValue(Parameter parameter, int depth) throws Inv return basicValue; } - // For constructor parameters: Collections/Maps/Arrays should be empty + // В конструкторе заполняем параметры пустыми коллекциями, + // так как могут быть, например, бизнес-валидации в конструкторе. + // Наполняем объект уже после инициализации. if (Collection.class.isAssignableFrom(rawType)) { return TypeHelpers.createEmptyCollection(rawType); }