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** Ваша задача --- написать генератор экземпляров произвольных классов. diff --git a/build.gradle.kts b/build.gradle.kts index b4738ae..bc98873 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,6 +5,12 @@ plugins { group = "org.example" version = "1.0-SNAPSHOT" +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(21)) + } +} + repositories { mavenCentral() } @@ -12,7 +18,9 @@ repositories { dependencies { testImplementation(platform("org.junit:junit-bom:5.10.0")) testImplementation("org.junit.jupiter:junit-jupiter") + testImplementation("org.assertj:assertj-core:3.27.6") testRuntimeOnly("org.junit.platform:junit-platform-launcher") + testImplementation("net.bytebuddy:byte-buddy:1.17.8") } tasks.test { diff --git a/src/main/java/org/example/GenerateExample.java b/src/main/java/org/example/GenerateExample.java deleted file mode 100644 index 47679a9..0000000 --- a/src/main/java/org/example/GenerateExample.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.example; - - -import org.example.classes.Example; -import org.example.generator.Generator; - -public class GenerateExample { - public static void main(String[] args) { - var gen = new Generator(); - try { - Object generated = gen.generateValueOfType(Example.class); - System.out.println(generated); - } catch (Throwable e) { - throw new RuntimeException(e); - } - } -} \ No newline at end of file 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..b7f934a --- /dev/null +++ b/src/main/java/org/example/generator/Generatable.java @@ -0,0 +1,11 @@ +package org.example.generator; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Generatable { +} diff --git a/src/main/java/org/example/generator/GenerationException.java b/src/main/java/org/example/generator/GenerationException.java new file mode 100644 index 0000000..a97b124 --- /dev/null +++ b/src/main/java/org/example/generator/GenerationException.java @@ -0,0 +1,7 @@ +package org.example.generator; + +public class GenerationException extends Exception { + public GenerationException(String message) { + super(message); + } +} diff --git a/src/main/java/org/example/generator/Generator.java b/src/main/java/org/example/generator/Generator.java index 9d86bfb..800a7b4 100644 --- a/src/main/java/org/example/generator/Generator.java +++ b/src/main/java/org/example/generator/Generator.java @@ -1,18 +1,306 @@ package org.example.generator; +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.util.Random; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.net.URISyntaxException; +import java.util.*; +import java.util.function.Supplier; +import org.example.generator.type.TypeGeneratorsProvider; public class Generator { - public Object generateValueOfType(Class clazz) throws InvocationTargetException, InstantiationException, IllegalAccessException { + private final Map, Supplier> generators; + + private final int maxDepth; + private final String packageToScan; + private final Set> classesInPackageToScan; + + private final Random random = new Random(); + + public Generator( + Collection providers, + int maxDepth, + Object packageMarker + ) { + Map, Supplier> result = new HashMap<>(); + + for (TypeGeneratorsProvider provider : providers) { + Map, Supplier> generatorsFromProvider = provider.getGenerators(); + + for (Map.Entry, Supplier> entry : generatorsFromProvider.entrySet()) { + Class type = entry.getKey(); + Supplier supplier = entry.getValue(); + + if (result.containsKey(type)) { + throw new IllegalArgumentException( + "Multiple providers supply generator for type: " + type.getName() + ); + } + + result.put(type, supplier); + } + } + + this.generators = Map.copyOf(result); + + if (maxDepth <= 0) { + throw new IllegalArgumentException("maxDepth expected to be more than 0, but got " + maxDepth); + } + this.maxDepth = maxDepth; + + this.packageToScan = packageMarker.getClass().getPackageName(); + // todo add test + try { + this.classesInPackageToScan = PackageUtils.getClassesInPackage( + packageToScan, + Thread.currentThread().getContextClassLoader() + ); + } catch (IOException | URISyntaxException e) { + throw new RuntimeException(e); + } + } + + public Object generateValueOfType( + Class clazz + ) throws InvocationTargetException, InstantiationException, IllegalAccessException, GenerationException { + return generateValueOfType(clazz, 0); + } + + private Object generateValueOfType( + Class clazz, + int depth + ) throws InvocationTargetException, InstantiationException, IllegalAccessException, GenerationException { + + if (!canBeGenerated(clazz)) { + throw new GenerationException( + "Class is not annotated with @" + Generatable.class.getSimpleName() + " and not a simple type" + ); + } + + // todo primitives cannot be null + if (depth > maxDepth) { + return null; + } + + if (generators.containsKey(clazz)) { + return generators.get(clazz).get(); + } + + if (clazz.isEnum()) { + return generateEnum(clazz); + } + + if (clazz.isArray()) { + return generateArray(clazz, depth); + } + + if (Collection.class.isAssignableFrom(clazz)) { + return generateCollectionFromClass(clazz); + } + + if (Map.class.isAssignableFrom(clazz)) { + return generateMapFromClass(clazz); + } + + if (clazz.isInterface()) { + Class implementationClass = findImplementationClass(clazz).orElseThrow( + () -> new GenerationException("No implementation found for interface " + clazz.getName()) + ); + return generateValueOfType(implementationClass, depth); // not incrementing depth on purpose + } + + return generateCommonClass(clazz, depth); + } + + private boolean canBeGenerated(Class clazz) { + return generators.containsKey(clazz) || + clazz.isEnum() || + clazz.isArray() || + Collection.class.isAssignableFrom(clazz) || + Map.class.isAssignableFrom(clazz) || + clazz.isAnnotationPresent(Generatable.class); + } + + // todo вынести длину в параметр + private Object generateArray( + Class arrayClass, + int depth + ) throws GenerationException, InvocationTargetException, InstantiationException, IllegalAccessException { + Class arrayElementClass = arrayClass.getComponentType(); + + if (arrayElementClass == null) { + throw new IllegalStateException("generateArray received not array as a parameter"); + } + + int length = random.nextInt(1, 10); + Object result = Array.newInstance(arrayElementClass, length); + + for (int i = 0; i < length; ++i) { + Object element = generateValueOfType(arrayElementClass, depth + 1); + Array.set(result, i, element); + } + + return result; + } + + private Object generateEnum(Class enumClass) throws GenerationException { + Object[] values = enumClass.getEnumConstants(); + + if (values == null) { + // should not happen + throw new IllegalStateException("generateEnum received not enum as a parameter"); + } + + if (0 == values.length) { + throw new GenerationException("enum '" + enumClass.getName() + + "' cannot generated, because values is empty" + ); + } + + return values[random.nextInt(values.length)]; + } + + private Collection generateCollectionFromClass(Class collectionClass) { + return switch (collectionClass) { + case Class c when Set.class.isAssignableFrom(c) -> new HashSet<>(); + case Class c when Queue.class.isAssignableFrom(c) -> new LinkedList<>(); + default -> new ArrayList<>(); + }; + } + + private Map generateMapFromClass(Class mapClass) { + if (SortedMap.class.isAssignableFrom(mapClass)) { + return new TreeMap<>(); + } + return new HashMap<>(); + } + + private Optional> findImplementationClass(Class interfaceClass) { + if (!interfaceClass.getPackageName().startsWith(packageToScan)) { + return Optional.empty(); + } + + List> implementations = classesInPackageToScan.stream().filter(c -> + interfaceClass.isAssignableFrom(c) && + c.isAnnotationPresent(Generatable.class) && + !c.isInterface() && + !Modifier.isAbstract(c.getModifiers()) + ).toList(); + if (implementations.isEmpty()) { + return Optional.empty(); + } + return Optional.of(implementations.get(random.nextInt(implementations.size()))); + } + + private Object generateCommonClass( + Class clazz, + int depth + ) throws GenerationException, InvocationTargetException, InstantiationException, IllegalAccessException { Constructor[] constructors = clazz.getDeclaredConstructors(); + for (int i = 0; i < constructors.length; i++) { + Constructor constructor = constructors[i]; + try { + return tryConstructor(constructor, depth); + } catch (Exception e) { + if (i == constructors.length - 1) { + throw e; + } + } + } - int randomConstructorIndex = new Random().nextInt(constructors.length); - Constructor randomConstructor = constructors[randomConstructorIndex]; - return randomConstructor.newInstance(111); + throw new GenerationException("No suitable constructor found for class: " + clazz.getName()); } + private Object tryConstructor( + Constructor constructor, + int depth + ) throws GenerationException, InvocationTargetException, InstantiationException, IllegalAccessException { + Object[] paramValues = new Object[constructor.getParameterCount()]; + + for (int i = 0; i < constructor.getParameterCount(); i++) { + Class paramType = constructor.getParameterTypes()[i]; + paramValues[i] = generateValueOfType(paramType, depth + 1); + } + + var instance = constructor.newInstance(paramValues); + + var clazz = instance.getClass(); + + for (Field field : clazz.getDeclaredFields()) { + int modifiers = field.getModifiers(); + if (Modifier.isStatic(modifiers) || Modifier.isFinal(modifiers)) { + continue; + } + + field.setAccessible(true); + Class fieldClass = field.getType(); + Object fieldValue = switch (fieldClass) { + case Class c when Collection.class.isAssignableFrom(c) -> generateCollectionFromField(field, depth); + case Class c when Map.class.isAssignableFrom(c) -> generateMapFromField(field, depth); + default -> generateValueOfType(fieldClass, depth + 1); + }; + + field.set(instance, fieldValue); + } + + return instance; + } + + private Collection generateCollectionFromField( + Field collectionField, + int depth + ) throws GenerationException, InvocationTargetException, InstantiationException, IllegalAccessException { + Type genericType = collectionField.getGenericType(); + Collection collection = generateCollectionFromClass(collectionField.getType()); + + if (genericType instanceof ParameterizedType parameterizedType) { + Type[] typeArgs = parameterizedType.getActualTypeArguments(); + + // if not generic, then fill + if (typeArgs.length == 1 && typeArgs[0] instanceof Class elementType) { + int length = random.nextInt(1, 10); + + for (int i = 0; i < length; ++i) { + Object element = generateValueOfType(elementType, depth + 1); + collection.add(element); + } + } + } + + return collection; + } + + private Map generateMapFromField( + Field mapField, + int depth + ) throws GenerationException, InvocationTargetException, InstantiationException, IllegalAccessException { + Type genericType = mapField.getGenericType(); + Map map = generateMapFromClass(mapField.getType()); + + if (genericType instanceof ParameterizedType parameterizedType) { + Type[] typeArgs = parameterizedType.getActualTypeArguments(); + + if (typeArgs.length == 2 && + typeArgs[0] instanceof Class keyType && + typeArgs[1] instanceof Class valueType) { + + int size = random.nextInt(1, 10); + + for (int i = 0; i < size; i++) { + Object key = generateValueOfType(keyType, depth + 1); + Object value = generateValueOfType(valueType, depth + 1); + map.put(key, value); + } + } + } + + return map; + } } diff --git a/src/main/java/org/example/generator/PackageUtils.java b/src/main/java/org/example/generator/PackageUtils.java new file mode 100644 index 0000000..cf38b77 --- /dev/null +++ b/src/main/java/org/example/generator/PackageUtils.java @@ -0,0 +1,80 @@ +package org.example.generator; + +import java.io.File; +import java.io.IOException; +import java.net.JarURLConnection; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +public final class PackageUtils { + + private PackageUtils() { + throw new IllegalStateException("Utility class"); + } + + public static Set> getClassesInPackage(String packageName, ClassLoader classLoader) + throws IOException, URISyntaxException { + Set> classes = new HashSet<>(); + String path = packageName.replace('.', '/'); + Enumeration resources = classLoader.getResources(path); + + while (resources.hasMoreElements()) { + URL resource = resources.nextElement(); + if ("file".equals(resource.getProtocol())) { + File directory = new File(resource.toURI()); + scanDirectory(directory, packageName, classes, classLoader); + } else if ("jar".equals(resource.getProtocol())) { + JarFile jar = ((JarURLConnection) resource.openConnection()).getJarFile(); + scanJar(jar, packageName, classes, classLoader); + } + } + + return classes; + } + + private static void scanDirectory(File dir, String packageName, Set> classes, ClassLoader classLoader) { + File[] files = dir.listFiles(); + if (files == null) return; + + for (File file : files) { + if (file.isDirectory()) { + String subPackage = packageName.isEmpty() ? file.getName() : packageName + "." + file.getName(); + scanDirectory(file, subPackage, classes, classLoader); + } else if (file.getName().endsWith(".class")) { + String className = file.getName().substring(0, file.getName().length() - 6); + if (!packageName.isEmpty()) { + className = packageName + "." + className; + } + try { + classes.add(classLoader.loadClass(className)); + } catch (ClassNotFoundException e) { + // ignore + } + } + } + } + + private static void scanJar(JarFile jar, String packageName, Set> classes, ClassLoader classLoader) { + String packagePath = packageName.replace('.', '/'); + Enumeration entries = jar.entries(); + + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + String name = entry.getName(); + + if (name.startsWith(packagePath) && name.endsWith(".class") && !entry.isDirectory()) { + String className = name.substring(0, name.length() - 6).replace('/', '.'); + try { + classes.add(classLoader.loadClass(className)); + } catch (ClassNotFoundException e) { + // ignore + } + } + } + } +} diff --git a/src/main/java/org/example/generator/type/TypeGeneratorsProvider.java b/src/main/java/org/example/generator/type/TypeGeneratorsProvider.java new file mode 100644 index 0000000..2d46063 --- /dev/null +++ b/src/main/java/org/example/generator/type/TypeGeneratorsProvider.java @@ -0,0 +1,9 @@ +package org.example.generator.type; + +import java.util.Map; +import java.util.function.Supplier; + +@FunctionalInterface +public interface TypeGeneratorsProvider { + Map, Supplier> getGenerators(); +} diff --git a/src/main/java/org/example/generator/type/impl/PrimitiveGeneratorsProvider.java b/src/main/java/org/example/generator/type/impl/PrimitiveGeneratorsProvider.java new file mode 100644 index 0000000..06a57ca --- /dev/null +++ b/src/main/java/org/example/generator/type/impl/PrimitiveGeneratorsProvider.java @@ -0,0 +1,45 @@ +package org.example.generator.type.impl; + +import java.util.Map; +import java.util.Random; +import java.util.function.Supplier; +import org.example.generator.type.TypeGeneratorsProvider; + +public class PrimitiveGeneratorsProvider implements TypeGeneratorsProvider { + + private final Random random; + + public PrimitiveGeneratorsProvider(Random random) { + this.random = random; + } + + @Override + public Map, Supplier> getGenerators() { + + return Map.ofEntries( + Map.entry(boolean.class, random::nextBoolean), + Map.entry(Boolean.class, random::nextBoolean), + + Map.entry(byte.class, () -> (byte) random.nextInt(Byte.MAX_VALUE)), + Map.entry(Byte.class, () -> (byte) random.nextInt(Byte.MAX_VALUE)), + + Map.entry(short.class, () -> (short) random.nextInt(Short.MAX_VALUE)), + Map.entry(Short.class, () -> (short) random.nextInt(Short.MAX_VALUE)), + + Map.entry(int.class, random::nextInt), + Map.entry(Integer.class, random::nextInt), + + Map.entry(long.class, random::nextLong), + Map.entry(Long.class, random::nextLong), + + Map.entry(float.class, random::nextFloat), + Map.entry(Float.class, random::nextFloat), + + Map.entry(double.class, random::nextDouble), + Map.entry(Double.class, random::nextDouble), + + Map.entry(char.class, () -> (char) random.nextInt(Character.MAX_VALUE)), + Map.entry(Character.class, () -> (char) random.nextInt(Character.MAX_VALUE)) + ); + } +} diff --git a/src/main/java/org/example/generator/type/impl/StringGeneratorsProvider.java b/src/main/java/org/example/generator/type/impl/StringGeneratorsProvider.java new file mode 100644 index 0000000..754b8eb --- /dev/null +++ b/src/main/java/org/example/generator/type/impl/StringGeneratorsProvider.java @@ -0,0 +1,30 @@ +package org.example.generator.type.impl; + +import java.util.Map; +import java.util.Random; +import java.util.function.Supplier; +import org.example.generator.type.TypeGeneratorsProvider; + +public class StringGeneratorsProvider implements TypeGeneratorsProvider { + + private final Supplier stringSupplier; + + public StringGeneratorsProvider(Random random, int maxLength) { + + this.stringSupplier = () -> { + int length = random.nextInt(maxLength); + StringBuilder sb = new StringBuilder(length); + for (int i = 0; i < length; i++) { + sb.append((char) (random.nextInt(26) + 'a')); + } + + return sb.toString(); + }; + } + + @Override + public Map, Supplier> getGenerators() { + + return Map.of(String.class, stringSupplier); + } +} diff --git a/src/main/java/org/example/classes/BinaryTreeNode.java b/src/test/java/org/example/classes/BinaryTreeNode.java similarity index 92% rename from src/main/java/org/example/classes/BinaryTreeNode.java rename to src/test/java/org/example/classes/BinaryTreeNode.java index 046ff56..52a9157 100644 --- a/src/main/java/org/example/classes/BinaryTreeNode.java +++ b/src/test/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/test/java/org/example/classes/Cart.java similarity index 89% rename from src/main/java/org/example/classes/Cart.java rename to src/test/java/org/example/classes/Cart.java index 965237d..0feefd3 100644 --- a/src/main/java/org/example/classes/Cart.java +++ b/src/test/java/org/example/classes/Cart.java @@ -1,7 +1,9 @@ package org.example.classes; import java.util.List; +import org.example.generator.Generatable; +@Generatable public class Cart { private List items; @@ -18,4 +20,4 @@ public void setItems(List items) { } // Конструктор, методы добавления и удаления товаров, геттеры и другие методы -} \ No newline at end of file +} diff --git a/src/test/java/org/example/classes/EmptyEnum.java b/src/test/java/org/example/classes/EmptyEnum.java new file mode 100644 index 0000000..890cc3e --- /dev/null +++ b/src/test/java/org/example/classes/EmptyEnum.java @@ -0,0 +1,3 @@ +package org.example.classes; + +public enum EmptyEnum {} diff --git a/src/main/java/org/example/classes/Example.java b/src/test/java/org/example/classes/Example.java similarity index 78% rename from src/main/java/org/example/classes/Example.java rename to src/test/java/org/example/classes/Example.java index eac9463..e0c8d1c 100644 --- a/src/main/java/org/example/classes/Example.java +++ b/src/test/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/test/java/org/example/classes/InterfaceWithNoImpl.java b/src/test/java/org/example/classes/InterfaceWithNoImpl.java new file mode 100644 index 0000000..dd4fd53 --- /dev/null +++ b/src/test/java/org/example/classes/InterfaceWithNoImpl.java @@ -0,0 +1,7 @@ +package org.example.classes; + +import org.example.generator.Generatable; + +@Generatable +public interface InterfaceWithNoImpl { +} diff --git a/src/test/java/org/example/classes/InternalMapTest.java b/src/test/java/org/example/classes/InternalMapTest.java new file mode 100644 index 0000000..c80da83 --- /dev/null +++ b/src/test/java/org/example/classes/InternalMapTest.java @@ -0,0 +1,17 @@ +package org.example.classes; + +import java.util.Map; +import org.example.generator.Generatable; + +@Generatable +public class InternalMapTest { + private Map items; + + public Map getItems() { + return items; + } + + public void setItems(Map items) { + this.items = items; + } +} diff --git a/src/test/java/org/example/classes/NonGeneratable.java b/src/test/java/org/example/classes/NonGeneratable.java new file mode 100644 index 0000000..4802552 --- /dev/null +++ b/src/test/java/org/example/classes/NonGeneratable.java @@ -0,0 +1,14 @@ +package org.example.classes; + +public class NonGeneratable { + int i; + + public NonGeneratable(int i) { + this.i = i; + } + + @Override + public String toString() { + return "NonGeneratable(" + i + ")"; + } +} diff --git a/src/main/java/org/example/classes/Product.java b/src/test/java/org/example/classes/Product.java similarity index 93% rename from src/main/java/org/example/classes/Product.java rename to src/test/java/org/example/classes/Product.java index e7dcc89..f439adf 100644 --- a/src/main/java/org/example/classes/Product.java +++ b/src/test/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; @@ -44,5 +47,4 @@ public boolean equals(Object obj) { public String toString() { return super.toString(); } - } diff --git a/src/main/java/org/example/classes/Rectangle.java b/src/test/java/org/example/classes/Rectangle.java similarity index 75% rename from src/main/java/org/example/classes/Rectangle.java rename to src/test/java/org/example/classes/Rectangle.java index 90b0886..5192246 100644 --- a/src/main/java/org/example/classes/Rectangle.java +++ b/src/test/java/org/example/classes/Rectangle.java @@ -1,8 +1,11 @@ package org.example.classes; +import org.example.generator.Generatable; + +@Generatable public class Rectangle implements Shape { - private double length; - private double width; + private final double length; + private final double width; public Rectangle(double length, double width) { this.length = length; diff --git a/src/main/java/org/example/classes/Shape.java b/src/test/java/org/example/classes/Shape.java similarity index 65% rename from src/main/java/org/example/classes/Shape.java rename to src/test/java/org/example/classes/Shape.java index c20a851..7a6f41f 100644 --- a/src/main/java/org/example/classes/Shape.java +++ b/src/test/java/org/example/classes/Shape.java @@ -1,5 +1,8 @@ package org.example.classes; +import org.example.generator.Generatable; + +@Generatable public interface Shape { double getArea(); double getPerimeter(); diff --git a/src/test/java/org/example/classes/TestEnum.java b/src/test/java/org/example/classes/TestEnum.java new file mode 100644 index 0000000..6749fc4 --- /dev/null +++ b/src/test/java/org/example/classes/TestEnum.java @@ -0,0 +1,13 @@ +package org.example.classes; + +public enum TestEnum { + ONE("one"), + TWO("two"), + ; + + final String name; + + TestEnum(String name) { + this.name = name; + } +} diff --git a/src/main/java/org/example/classes/Triangle.java b/src/test/java/org/example/classes/Triangle.java similarity index 76% rename from src/main/java/org/example/classes/Triangle.java rename to src/test/java/org/example/classes/Triangle.java index 011e96f..c68ee83 100644 --- a/src/main/java/org/example/classes/Triangle.java +++ b/src/test/java/org/example/classes/Triangle.java @@ -1,9 +1,12 @@ package org.example.classes; +import org.example.generator.Generatable; + +@Generatable public class Triangle implements Shape { - private double sideA; - private double sideB; - private double sideC; + private final double sideA; + private final double sideB; + private final double sideC; public Triangle(double sideA, double sideB, double sideC) { this.sideA = sideA; 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..6c1bb16 --- /dev/null +++ b/src/test/java/org/example/generator/GeneratorTest.java @@ -0,0 +1,223 @@ +package org.example.generator; + +import java.util.*; +import org.example.classes.*; +import org.example.generator.type.TypeGeneratorsProvider; +import org.example.generator.type.impl.PrimitiveGeneratorsProvider; +import org.example.generator.type.impl.StringGeneratorsProvider; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class GeneratorTest { + + private final Random random = new Random(); + + private final Collection providers = List.of( + new PrimitiveGeneratorsProvider(random), + new StringGeneratorsProvider(random, 15) + ); + private final Object marker = TestEnum.ONE; + + private final Generator generator = new Generator(providers, 10, marker); + + @Test + void shouldThrowOnDuplicateGenerator() { + TypeGeneratorsProvider provider1 = () -> Map.of(String.class, () -> "test-string"); + TypeGeneratorsProvider provider2 = () -> Map.of(String.class, () -> "test-string-2"); + + Collection duplicateProviders = List.of(provider1, provider2); + + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> new Generator(duplicateProviders, 1, marker) + ); + + assertThat(exception.getMessage()) + .contains("Multiple providers supply generator for type: java.lang.String"); + } + + @ParameterizedTest + @ValueSource(ints = {-1, 0}) + void shouldThrowOnNegativeOrZeroMaxDepth(int maxDepth) { + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> new Generator(List.of(), maxDepth, marker) + ); + + assertThat(exception.getMessage()) + .isEqualTo("maxDepth expected to be more than 0, but got " + maxDepth); + } + + @Test + void shouldThrowExceptionOnNonGeneratableClass() { + var ex = assertThrows( + GenerationException.class, + () -> generator.generateValueOfType(NonGeneratable.class) + ); + assertThat(ex.getMessage()).isEqualTo( + "Class is not annotated with @Generatable and not a simple type" + ); + } + + @Test + void shouldGenerateSimpleClassesFromGenerators() { + TypeGeneratorsProvider provider = () -> Map.of(String.class, () -> "test-string"); + Generator generator = new Generator(List.of(provider), 1, marker); + + assertThat(generate(generator, String.class)).isEqualTo("test-string"); + } + + @Test + void shouldGenerateEnum() { + Generator generator = new Generator(List.of(), 1, marker); + + assertThat(generate(generator, TestEnum.class)).isInstanceOf(TestEnum.class); + } + + @Test + void shouldThrowOnEmptyEnum() { + var ex = assertThrows( + GenerationException.class, + () -> generator.generateValueOfType(EmptyEnum.class) + ); + assertThat(ex.getMessage()).isEqualTo("enum '" + EmptyEnum.class.getName() + + "' cannot generated, because values is empty" + ); + } + + @Test + void shouldGenerateArray() { + Class intArrayClass = int[].class; + assertThat(generate(intArrayClass)).isInstanceOf(intArrayClass); + } + + @Test + void shouldGenerate2DArray() { + Class intArrayClass = int[][].class; + assertThat(generate(intArrayClass)).isInstanceOf(intArrayClass); + } + + @Test + void shouldPlaceNullIfMaxDepthReached() { + TypeGeneratorsProvider provider = () -> Map.of(String.class, () -> "test-string"); + var generator = new Generator(List.of(provider), 1, marker); + var result = generate(generator, String[][].class); + + assertThat(result).isInstanceOf(String[][].class); + + var resultTyped = (String[][]) result; + assertThat(resultTyped[0][0]).isNull(); + } + + @Test + void shouldGenerateList() { + var result = generate(List.class); + assertThat(result).isInstanceOf(ArrayList.class); + assertThat(((List) result).size()).isEqualTo(0); + } + + @Test + void shouldGenerateSet() { + var result = generate(Set.class); + assertThat(result).isInstanceOf(HashSet.class); + assertThat(((Set) result).size()).isEqualTo(0); + } + + @Test + void shouldGenerateQueue() { + var result = generate(Queue.class); + assertThat(result).isInstanceOf(LinkedList.class); + assertThat(((Queue) result).size()).isEqualTo(0); + } + + @Test + void shouldGenerateCollection() { + var result = generate(Collection.class); + assertThat(result).isInstanceOf(ArrayList.class); + assertThat(((Collection) result).size()).isEqualTo(0); + } + + @Test + void shouldGenerateMap() { + var result = generate(Map.class); + assertThat(result).isInstanceOf(HashMap.class); + assertThat(((Map) result).size()).isEqualTo(0); + } + + @Test + void shouldGenerateSortedMap() { + var result = generate(SortedMap.class); + assertThat(result).isInstanceOf(TreeMap.class); + assertThat(((Map) result).size()).isEqualTo(0); + } + + @Test + void shouldFillInternalCollection() { + var instance = generate(Cart.class); + assertThat(instance).isInstanceOf(Cart.class); + + var cart = (Cart) instance; + var items = cart.getItems(); + assertThat(items.size()).isNotEqualTo(0); + assertThat(items.getFirst()).isInstanceOf(Product.class); + } + + @Test + void shouldFillInternalMap() { + var instance = generate(InternalMapTest.class); + assertThat(instance).isInstanceOf(InternalMapTest.class); + + var cart = (InternalMapTest) instance; + var items = cart.getItems(); + assertThat(items.size()).isNotEqualTo(0); + + Map.Entry item = items.entrySet().iterator().next(); + assertThat(item.getKey()).isInstanceOf(Product.class); + assertThat(item.getValue()).isInstanceOf(String.class); + } + + @Test + void shouldThrowOnInterfaceWithNotImpl() { + var clazz = InterfaceWithNoImpl.class; + var ex = assertThrows( + GenerationException.class, + () -> generator.generateValueOfType(clazz) + ); + assertThat(ex.getMessage()).isEqualTo("No implementation found for interface " + clazz.getName()); + } + + @ParameterizedTest + @MethodSource("source") + void shouldGenerateSupportedClasses(Class clazz) { + var instance = generate(clazz); + assertThat(instance).isInstanceOf(clazz); + } + + static List> source() { + return List.of( + Example.class, + Product.class, + Cart.class, + BinaryTreeNode.class, + Rectangle.class, + Triangle.class, + Shape.class + ); + } + + private Object generate(Class clazz) { + return generate(generator, clazz); + } + + private Object generate(Generator generator, Class clazz) { + try { + return generator.generateValueOfType(clazz); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/test/java/org/example/generator/PackageUtilsTest.java b/src/test/java/org/example/generator/PackageUtilsTest.java new file mode 100644 index 0000000..8341065 --- /dev/null +++ b/src/test/java/org/example/generator/PackageUtilsTest.java @@ -0,0 +1,63 @@ +package org.example.generator; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Path; +import java.util.List; +import java.util.Set; +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.dynamic.DynamicType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +class PackageUtilsTest { + + @TempDir + private Path tempDir; + + private URL tempUrl; + + private final ByteBuddy byteBuddy = new ByteBuddy(); + private final String packageName = "org.example.kek"; + + @BeforeEach + void setUp() throws IOException { + + saveClassUsingByteBuddy(packageName + ".Service1"); + saveClassUsingByteBuddy(packageName + ".Service2"); + saveClassUsingByteBuddy(packageName + ".utils.Helper"); + + tempUrl = tempDir.toUri().toURL(); + } + + private void saveClassUsingByteBuddy(String name) throws IOException { + try (DynamicType object = byteBuddy.subclass(Object.class).name(name).make()) { + object.saveIn(tempDir.toFile()); + } + } + + @Test + void shouldFindAllImpl() throws IOException, URISyntaxException { + try (var classLoader = new URLClassLoader( + new URL[]{tempUrl}, + Thread.currentThread().getContextClassLoader() + ) + ) { + Set> result = PackageUtils.getClassesInPackage(packageName, classLoader); + + assertThat(result.size()).isEqualTo(3); + List.of( + packageName + ".Service1", + packageName + ".Service2", + packageName + ".utils.Helper" + ).forEach(name -> assertThat( + result.stream().anyMatch(c -> c.getName().equals(name)) + ).isTrue() + ); + } + } +} diff --git a/src/test/java/org/example/generator/type/TypeGeneratorsProviderTest.java b/src/test/java/org/example/generator/type/TypeGeneratorsProviderTest.java new file mode 100644 index 0000000..acc5ca9 --- /dev/null +++ b/src/test/java/org/example/generator/type/TypeGeneratorsProviderTest.java @@ -0,0 +1,55 @@ +package org.example.generator.type; + +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +public abstract class TypeGeneratorsProviderTest { + + protected abstract TypeGeneratorsProvider getProvider(); + + @Test + void suppliersShouldMatchClasses() { + var provider = getProvider(); + + var generators = provider.getGenerators(); + + for (var classToSupplier : generators.entrySet()) { + var clazz = classToSupplier.getKey(); + var supplier = classToSupplier.getValue(); + + var instance = supplier.get(); + + assertThat(instance).isInstanceOfAny(clazz, getPrimitiveType(clazz), getWrapperType(clazz)); + } + } + + private Class getPrimitiveType(Class wrapper) { + return switch (wrapper.getSimpleName()) { + case "java.lang.Integer" -> int.class; + case "java.lang.Short" -> short.class; + case "java.lang.Byte" -> byte.class; + case "java.lang.Long" -> long.class; + case "java.lang.Float" -> float.class; + case "java.lang.Double" -> double.class; + case "java.lang.Character" -> char.class; + case "java.lang.Boolean" -> boolean.class; + case "java.lang.Void" -> void.class; + default -> wrapper; + }; + } + + private Class getWrapperType(Class primitive) { + return switch (primitive.getSimpleName()) { + case "int" -> Integer.class; + case "short" -> Short.class; + case "byte" -> Byte.class; + case "long" -> Long.class; + case "float" -> Float.class; + case "double" -> Double.class; + case "char" -> Character.class; + case "boolean" -> Boolean.class; + case "void" -> Void.class; + default -> primitive; + }; + } +} diff --git a/src/test/java/org/example/generator/type/impl/PrimitiveGeneratorsProviderTest.java b/src/test/java/org/example/generator/type/impl/PrimitiveGeneratorsProviderTest.java new file mode 100644 index 0000000..578320c --- /dev/null +++ b/src/test/java/org/example/generator/type/impl/PrimitiveGeneratorsProviderTest.java @@ -0,0 +1,16 @@ +package org.example.generator.type.impl; + +import java.util.Random; +import org.example.generator.type.TypeGeneratorsProvider; +import org.example.generator.type.TypeGeneratorsProviderTest; + +class PrimitiveGeneratorsProviderTest extends TypeGeneratorsProviderTest { + + private final Random random = new Random(); + private final TypeGeneratorsProvider provider = new PrimitiveGeneratorsProvider(random); + + @Override + protected TypeGeneratorsProvider getProvider() { + return provider; + } +} diff --git a/src/test/java/org/example/generator/type/impl/StringGeneratorsProviderTest.java b/src/test/java/org/example/generator/type/impl/StringGeneratorsProviderTest.java new file mode 100644 index 0000000..199228f --- /dev/null +++ b/src/test/java/org/example/generator/type/impl/StringGeneratorsProviderTest.java @@ -0,0 +1,26 @@ +package org.example.generator.type.impl; + +import java.util.Random; +import org.example.generator.type.TypeGeneratorsProvider; +import org.example.generator.type.TypeGeneratorsProviderTest; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +class StringGeneratorsProviderTest extends TypeGeneratorsProviderTest { + + private final Random random = new Random(); + private final TypeGeneratorsProvider provider = new StringGeneratorsProvider(random, 15); + + @Override + protected TypeGeneratorsProvider getProvider() { + return provider; + } + + @Test + void shouldGenerateStrings() { + var generators = provider.getGenerators(); + + assertThat(generators.size()).isEqualTo(1); + assertThat(generators.containsKey(String.class)).isTrue(); + } +} diff --git a/types.md b/types.md new file mode 100644 index 0000000..cb2c9bc --- /dev/null +++ b/types.md @@ -0,0 +1,9 @@ +- [X] [Example.java](src/test/java/org/example/classes/Example.java): базовый пример, содержит один примитив в + конструкторе +- [X] [Product.java](src/test/java/org/example/classes/Product.java): конструктор принимает на вход не все поля, + примитивы +- [X] [Cart.java](src/test/java/org/example/classes/Cart.java): конструктор принимает на вход класс из classpath +- [X] [BinaryTreeNode.java](src/test/java/org/example/classes/BinaryTreeNode.java): рекурсия +- [X] [Shape.java](src/test/java/org/example/classes/Shape.java): интерфейс (предположительно выбираем любую реализация) + - [X] [Rectangle.java](src/test/java/org/example/classes/Rectangle.java): реализация один + - [X] [Triangle.java](src/test/java/org/example/classes/Triangle.java): реализация два