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..a622996 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -9,6 +9,15 @@ repositories { mavenCentral() } +java { + sourceCompatibility = JavaVersion.VERSION_21 +} + +dependencies { + implementation("org.slf4j:slf4j-api:2.0.17") + implementation("ch.qos.logback:logback-classic:1.5.18") +} + dependencies { testImplementation(platform("org.junit:junit-bom:5.10.0")) testImplementation("org.junit.jupiter:junit-jupiter") diff --git a/src/main/java/org/example/GenerateExample.java b/src/main/java/org/example/GenerateExample.java index 47679a9..5110096 100644 --- a/src/main/java/org/example/GenerateExample.java +++ b/src/main/java/org/example/GenerateExample.java @@ -1,17 +1,64 @@ package org.example; +import org.example.classes.BinaryTreeNode; +import org.example.classes.Cart; import org.example.classes.Example; +import org.example.classes.Product; +import org.example.classes.Shape; +import org.example.classes.Triangle; import org.example.generator.Generator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.InvocationTargetException; public class GenerateExample { + private static final String PACKAGE_NAME = "org.example"; + private static final Logger log = LoggerFactory.getLogger(GenerateExample.class); + public static void main(String[] args) { var gen = new Generator(); try { - Object generated = gen.generateValueOfType(Example.class); - System.out.println(generated); + generateWithAnnotatedAbstractClass(gen); + generateRandomInterfaceImplementation(gen); + generateRecursiveDataStructure(gen); + generateSimpleClass(gen); + generateWithCollections(gen); + generateTriangle(gen); } catch (Throwable e) { throw new RuntimeException(e); } } + + private static void generateWithAnnotatedAbstractClass(Generator generator) throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException { + Object generated = generator.generateByType(Example.class, PACKAGE_NAME); + log.info("Generated: {}", generated); + } + + private static void generateRandomInterfaceImplementation(Generator generator) throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException { + Object generated = generator.generateByType(Shape.class, PACKAGE_NAME); + log.info("Generated: {}", generated); + } + + private static void generateRecursiveDataStructure(Generator generator) throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException { + Object generated = generator.generateByType(BinaryTreeNode.class, PACKAGE_NAME); + log.info("Generated: {}", generated); + } + + private static void generateSimpleClass(Generator generator) throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException { + Object generated = generator.generateByType(Product.class, PACKAGE_NAME); + log.info("Generated: {}", generated); + } + + private static void generateWithCollections(Generator generator) throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException { + Object generated = generator.generateByType(Cart.class, PACKAGE_NAME); + log.info("Generated: {}", generated); + } + + private static void generateTriangle(Generator generator) throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException { + Triangle generated = (Triangle) generator.generateByType(Triangle.class, PACKAGE_NAME); + log.info("Generated: {}", generated); + log.info("Triangle perimeter is = {}", generated.getPerimeter()); + } } \ No newline at end of file diff --git a/src/main/java/org/example/annotation/Generatable.java b/src/main/java/org/example/annotation/Generatable.java new file mode 100644 index 0000000..81feeeb --- /dev/null +++ b/src/main/java/org/example/annotation/Generatable.java @@ -0,0 +1,11 @@ +package org.example.annotation; + +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 { +} diff --git a/src/main/java/org/example/classes/AbstractExample.java b/src/main/java/org/example/classes/AbstractExample.java new file mode 100644 index 0000000..9113726 --- /dev/null +++ b/src/main/java/org/example/classes/AbstractExample.java @@ -0,0 +1,12 @@ +package org.example.classes; + +import org.example.annotation.Generatable; + +@Generatable +public abstract class AbstractExample { + protected int x; + + public AbstractExample(int x) { + this.x = x; + } +} 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..3896913 100644 --- a/src/main/java/org/example/classes/Cart.java +++ b/src/main/java/org/example/classes/Cart.java @@ -1,12 +1,18 @@ package org.example.classes; +import org.example.annotation.Generatable; + import java.util.List; +import java.util.Map; +@Generatable public class Cart { private List items; + private Map products; - public Cart(List items) { + public Cart(List items,Map products) { this.items = items; + this.products = products; } public List getItems() { @@ -17,5 +23,13 @@ public void setItems(List items) { this.items = items; } - // Конструктор, методы добавления и удаления товаров, геттеры и другие методы +// Конструктор, методы добавления и удаления товаров, геттеры и другие методы + + @Override + public String toString() { + return "Cart{" + + "items=" + items + + ", products=" + products + + '}'; + } } \ 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..0f0e998 100644 --- a/src/main/java/org/example/classes/Example.java +++ b/src/main/java/org/example/classes/Example.java @@ -1,14 +1,22 @@ package org.example.classes; -public class Example { +public class Example extends AbstractExample { int i; - public Example(int i) { + public Example(int i, int x) { + super(x); this.i = i; } + public Example(int x) { + super(x); + } + @Override public String toString() { - return "Example(" + i + ")"; + return "Example{" + + "i=" + i + + ", x=" + x + + '}'; } } diff --git a/src/main/java/org/example/classes/Product.java b/src/main/java/org/example/classes/Product.java index e7dcc89..6a066d3 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; @@ -42,7 +45,9 @@ public boolean equals(Object 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..e0d6bdf 100644 --- a/src/main/java/org/example/classes/Rectangle.java +++ b/src/main/java/org/example/classes/Rectangle.java @@ -18,4 +18,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/Shape.java b/src/main/java/org/example/classes/Shape.java index c20a851..ee60ced 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.annotation.Generatable; + +@Generatable 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..cae61f2 100644 --- a/src/main/java/org/example/classes/Triangle.java +++ b/src/main/java/org/example/classes/Triangle.java @@ -21,4 +21,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/Generator.java b/src/main/java/org/example/generator/Generator.java index 9d86bfb..b8ad5f6 100644 --- a/src/main/java/org/example/generator/Generator.java +++ b/src/main/java/org/example/generator/Generator.java @@ -1,18 +1,175 @@ package org.example.generator; +import org.example.annotation.Generatable; + import java.lang.reflect.Constructor; +import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.ParameterizedType; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; import java.util.Random; +import java.util.Set; +import java.util.concurrent.ThreadLocalRandom; +import java.util.function.Supplier; + +import static org.example.generator.util.ReflectionUtil.getAnnotatedClass; public class Generator { - public Object generateValueOfType(Class clazz) throws InvocationTargetException, InstantiationException, IllegalAccessException { - Constructor[] constructors = clazz.getDeclaredConstructors(); + private static final int MAX_DEPTH = 2; + + private static final Map, Supplier> PRIMITIVE_TYPES_GENERATORS = Map.ofEntries( + Map.entry(int.class, () -> new Random().nextInt(100)), + Map.entry(Integer.class, () -> new Random().nextInt(100)), + Map.entry(long.class, () -> new Random().nextLong(100)), + Map.entry(Long.class, () -> new Random().nextLong(100)), + Map.entry(double.class, () -> new Random().nextDouble(100)), + Map.entry(Double.class, () -> new Random().nextDouble(100)), + Map.entry(float.class, () -> new Random().nextFloat(100)), + Map.entry(Float.class, () -> new Random().nextFloat(100)), + Map.entry(boolean.class, () -> new Random().nextBoolean()), + Map.entry(Boolean.class, () -> new Random().nextBoolean()), + Map.entry(char.class, () -> (char) (new Random().nextInt(26) + 'a')), + Map.entry(Character.class, () -> (char) (new Random().nextInt(26) + 'a')), + Map.entry(String.class, getRandomString()) + ); + + private static final Set> IMMUTABLE_KEY_TYPES = PRIMITIVE_TYPES_GENERATORS.keySet(); - int randomConstructorIndex = new Random().nextInt(constructors.length); - Constructor randomConstructor = constructors[randomConstructorIndex]; - return randomConstructor.newInstance(111); + public Object generateByType(Class clazz, String packageName) throws IllegalArgumentException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException { + var annotatedClass = getAnnotatedClass(clazz, Generatable.class, packageName); + + if (annotatedClass.isEmpty()) { + throw new IllegalArgumentException(clazz.getCanonicalName() + " is not annotated with @" + Generatable.class.getCanonicalName()); + } + + return createInstance(annotatedClass.get(), 0); } + private static Object createInstance(Class clazz, int depth) throws InvocationTargetException, InstantiationException, IllegalAccessException { + if (PRIMITIVE_TYPES_GENERATORS.containsKey(clazz)) { + return PRIMITIVE_TYPES_GENERATORS.get(clazz).get(); + } + + if (depth > MAX_DEPTH) { + return null; + } + + if (Collection.class.isAssignableFrom(clazz)) { + return new ArrayList<>(); + } + + if (Map.class.isAssignableFrom(clazz)) { + return new HashMap<>(); + } + + var constructors = clazz.getDeclaredConstructors(); + var randomConstructorIndex = constructors.length == 0 ? 0 : new Random().nextInt(constructors.length); + var constructor = constructors[randomConstructorIndex]; + constructor.setAccessible(true); + + var paramTypes = constructor.getParameterTypes(); + var params = new Object[paramTypes.length]; + + for (int i = 0; i < paramTypes.length; i++) { + params[i] = createInstance(paramTypes[i], depth + 1); + } + + var instance = constructor.newInstance(params); + + for (var field : clazz.getDeclaredFields()) { + field.setAccessible(true); + var fieldType = field.getType(); + Object value; + if (Collection.class.isAssignableFrom(fieldType)) { + value = generateCollection(field, depth + 1); + } else if (Map.class.isAssignableFrom(fieldType)) { + value = generateMap(field, depth + 1); + } else if (PRIMITIVE_TYPES_GENERATORS.containsKey(fieldType)) { + value = PRIMITIVE_TYPES_GENERATORS.get(fieldType).get(); + } else { + value = createInstance(fieldType, depth + 1); + } + + field.set(instance, value); + } + return instance; + } + + private static Object generateCollection(Field field, int depth) throws InvocationTargetException, InstantiationException, IllegalAccessException { + var collection = new ArrayList<>(); + + var genericType = field.getGenericType(); + if (genericType instanceof ParameterizedType parameterizedType) { + var typeArgs = parameterizedType.getActualTypeArguments(); + if (typeArgs.length == 1 && typeArgs[0] instanceof Class elementClass) { + int size = new Random().nextInt(3) + 1; + for (int i = 0; i < size; i++) { + collection.add(createInstance(elementClass, depth + 1)); + } + } + } + + return collection; + } + + private static Object generateMap(Field field, int depth) + throws InvocationTargetException, InstantiationException, IllegalAccessException { + var map = new HashMap<>(); + + var genericType = field.getGenericType(); + if (genericType instanceof ParameterizedType parameterizedType) { + var typeArgs = parameterizedType.getActualTypeArguments(); + if (typeArgs.length == 2 && typeArgs[0] instanceof Class keyClass && typeArgs[1] instanceof Class valueClass) { + int size = new Random().nextInt(3) + 1; + for (int i = 0; i < size; i++) { + Object key; + if (keyClass.isEnum() || IMMUTABLE_KEY_TYPES.contains(keyClass)) { + key = createInstance(keyClass, depth + 1); + } else { + throw new IllegalArgumentException("key for Map is not immutable"); + } + Object value = createInstance(valueClass, depth + 1); + map.put(key, value); + } + } + } + + return map; + } + + private static Supplier getRandomString() { + return () -> { + ThreadLocalRandom random = ThreadLocalRandom.current(); + int length = 10; + char[] chars = new char[length]; + + for (int i = 0; i < length; i++) { + boolean lower = random.nextBoolean(); + char left = lower ? 'a' : 'A'; + char right = lower ? 'z' : 'Z'; + chars[i] = (char) random.nextInt(left, right + 1); + } + + return new String(chars); + }; + } } + +/*todo описание алгоритма работы генератора: +* 1) Проверить чем является переданный класс - классом или интерфейсом +* + Если интерфейс, то: +* а) Если ни одной реализации интерфейса нет, то выбросить исключение +* б) Найти любую реализацию интерфейса и выбрать ее для создания объекта +* + Если класс, то: +* a) Дойти до максимального родителя (Object) и посмотреть есть ли у него аннотация Generatable +* - Если нет, то выбросить исключение +* 2) Теперь мы выбрали класс, на основе которого будет создаваться объект +* +* +* + */ diff --git a/src/main/java/org/example/generator/util/ReflectionUtil.java b/src/main/java/org/example/generator/util/ReflectionUtil.java new file mode 100644 index 0000000..9fddf6f --- /dev/null +++ b/src/main/java/org/example/generator/util/ReflectionUtil.java @@ -0,0 +1,86 @@ +package org.example.generator.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.lang.annotation.Annotation; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Random; + +public class ReflectionUtil { + + private static final Logger log = LoggerFactory.getLogger(ReflectionUtil.class); + + public static Optional> getAnnotatedClass( + final Class clazz, + final Class annotation, + final String packageName + ) { + var clazzCopy = clazz; + var packageNameForClasses = new StringBuilder().append(packageName).append(".classes"); + + var isInterfaceAnnotated = false; + + if (clazzCopy.isInterface()) { + isInterfaceAnnotated = clazzCopy.isAnnotationPresent(annotation); + var implementations = findAllInterfaceImplementations(clazzCopy, packageNameForClasses.toString()); + clazzCopy = implementations.get(new Random().nextInt(implementations.size())); + log.debug("Was given an interface = {}, chosen implementation = {}", clazz.getCanonicalName(), clazzCopy.getCanonicalName()); + } + + while (!clazzCopy.equals(Object.class)) { + var implementedInterfaces = Arrays.asList(clazzCopy.getInterfaces()); + if (isInterfaceAnnotated) { + return Optional.of(clazzCopy); + } + if (clazzCopy.isAnnotationPresent(annotation) || + implementedInterfaces.stream().anyMatch(i -> i.isAnnotationPresent(annotation))) { + return Optional.of(clazz); + } + clazzCopy = clazzCopy.getSuperclass(); + } + return Optional.empty(); + } + + public static List> findAllInterfaceImplementations(Class interfaceClass, String packageName) { + var path = packageName.replace('.', '/'); + var classLoader = Thread.currentThread().getContextClassLoader(); + var resource = classLoader.getResource(path); + if (resource == null) { + return Collections.emptyList(); + } + + File directory; + try { + directory = new File(resource.toURI()); + } catch (URISyntaxException e) { + log.error("Invalid URI syntax: {}", e.getMessage()); + return Collections.emptyList(); + } + + var implementations = new ArrayList>(); + + for (var file : Objects.requireNonNull(directory.listFiles())) { + if (file.getName().endsWith(".class")) { + var className = packageName + "." + file.getName().replace(".class", ""); + try { + var clazz = Class.forName(className); + if (interfaceClass.isAssignableFrom(clazz) && !clazz.isInterface()) { + implementations.add(clazz); + } + } catch (ClassNotFoundException e) { + log.error("Class not found: {}", e.getMessage()); + } + } + } + + return implementations; + } +}