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/src/main/java/org/example/GenerateExample.java b/src/main/java/org/example/GenerateExample.java index 47679a9..1a436fb 100644 --- a/src/main/java/org/example/GenerateExample.java +++ b/src/main/java/org/example/GenerateExample.java @@ -1,15 +1,32 @@ package org.example; - +import org.example.classes.BinaryTreeNode; +import org.example.classes.Cart; import org.example.classes.Example; -import org.example.generator.Generator; +import org.example.classes.Product; +import org.example.classes.Rectangle; +import org.example.classes.Shape; +import org.example.classes.Triangle; +import org.example.generator.InstanceGenerator; public class GenerateExample { public static void main(String[] args) { - var gen = new Generator(); + var instanceGenerator = new InstanceGenerator(); try { - Object generated = gen.generateValueOfType(Example.class); - System.out.println(generated); + var generated1 = instanceGenerator.generateValueOf(Example.class); + System.out.println(generated1); + var generated2 = instanceGenerator.generateValueOf(Cart.class); + System.out.println(generated2); + var generated3 = instanceGenerator.generateValueOf(Product.class); + System.out.println(generated3); + var generated4 = instanceGenerator.generateValueOf(Rectangle.class); + System.out.println(generated4); + var generated5 = instanceGenerator.generateValueOf(Triangle.class); + System.out.println(generated5); + var generated6 = instanceGenerator.generateValueOf(Shape.class); + System.out.println(generated6); + var generated7 = instanceGenerator.generateValueOf(BinaryTreeNode.class); + System.out.println(generated7); } catch (Throwable e) { throw new RuntimeException(e); } diff --git a/src/main/java/org/example/classes/BinaryTreeNode.java b/src/main/java/org/example/classes/BinaryTreeNode.java index 046ff56..66588c5 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.annotation.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..38ca257 100644 --- a/src/main/java/org/example/classes/Cart.java +++ b/src/main/java/org/example/classes/Cart.java @@ -1,7 +1,10 @@ package org.example.classes; +import org.example.generator.annotation.Generatable; + import java.util.List; +@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..e452d02 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.annotation.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..96e8701 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.annotation.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..3bb6af5 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.annotation.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..948bfe2 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.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..c6a3ae3 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.annotation.Generatable; + +@Generatable public class Triangle implements Shape { private double sideA; private double sideB; diff --git a/src/main/java/org/example/common/RandomProvider.java b/src/main/java/org/example/common/RandomProvider.java new file mode 100644 index 0000000..a6ff502 --- /dev/null +++ b/src/main/java/org/example/common/RandomProvider.java @@ -0,0 +1,7 @@ +package org.example.common; + +import java.util.Random; + +public class RandomProvider { + public static final Random RANDOM = new Random(); +} diff --git a/src/main/java/org/example/generator/ClassPathScanner.java b/src/main/java/org/example/generator/ClassPathScanner.java new file mode 100644 index 0000000..adde069 --- /dev/null +++ b/src/main/java/org/example/generator/ClassPathScanner.java @@ -0,0 +1,59 @@ +package org.example.generator; + +import org.example.generator.annotation.Generatable; + +import java.io.File; +import java.util.Arrays; +import java.util.Collections; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class ClassPathScanner { + + static Set> scanForAnnotatedClasses() { + var classpath = System.getProperty("java.class.path"); + if (classpath == null || classpath.isEmpty()) { + return Collections.emptySet(); + } + + return Arrays.stream(classpath.split(File.pathSeparator)) + .map(File::new) + .filter(File::exists) + .filter(File::isDirectory) + .flatMap(dir -> scanDirectory(dir, "").stream()) + .collect(Collectors.toSet()); + } + + private static Set> scanDirectory(File dir, String targetPackage) { + var files = dir.listFiles(); + if (files == null) { + return Collections.emptySet(); + } + + return Arrays.stream(files) + .flatMap(file -> { + if (file.isDirectory()) { + var sub = targetPackage.isEmpty() ? file.getName() : targetPackage + "." + file.getName(); + return scanDirectory(file, sub).stream(); + } else if (file.getName().endsWith(".class")) { + var name = targetPackage + "." + file.getName().substring(0, file.getName().length() - 6); + return tryAddClass(name).stream(); + } + return Stream.empty(); + }) + .collect(Collectors.toSet()); + } + + private static Optional> tryAddClass(String className) { + try { + var name = Class.forName(className); + if (name.isAnnotationPresent(Generatable.class)) { + return Optional.of(name); + } + } catch (Throwable ignored) {} + + return Optional.empty(); + } +} diff --git a/src/main/java/org/example/generator/Generator.java b/src/main/java/org/example/generator/Generator.java deleted file mode 100644 index 9d86bfb..0000000 --- a/src/main/java/org/example/generator/Generator.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.example.generator; - -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.util.Random; - -public class Generator { - - public Object generateValueOfType(Class clazz) throws InvocationTargetException, InstantiationException, IllegalAccessException { - Constructor[] constructors = clazz.getDeclaredConstructors(); - - int randomConstructorIndex = new Random().nextInt(constructors.length); - Constructor randomConstructor = constructors[randomConstructorIndex]; - return randomConstructor.newInstance(111); - } - - -} diff --git a/src/main/java/org/example/generator/ImplementationIndex.java b/src/main/java/org/example/generator/ImplementationIndex.java new file mode 100644 index 0000000..e85ac1f --- /dev/null +++ b/src/main/java/org/example/generator/ImplementationIndex.java @@ -0,0 +1,52 @@ +package org.example.generator; + +import org.example.common.RandomProvider; +import org.example.generator.annotation.Generatable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +public class ImplementationIndex { + + private final Set> annotatedTypes; + private final Map, List>> interfaceToImplementations = new HashMap<>(); + + public ImplementationIndex(Set> annotatedTypes) { + this.annotatedTypes = Objects.requireNonNull(annotatedTypes); + buildIndex(); + } + + public Optional> randomImplementationFor(Class currentInterface) { + return Optional.ofNullable(interfaceToImplementations.get(currentInterface)) + .filter(list -> !list.isEmpty()) + .map(list -> list.get(RandomProvider.RANDOM.nextInt(list.size()))); + } + + private void buildIndex() { + annotatedTypes.stream() + .flatMap(currentClass -> { + Generatable annotation = currentClass.getAnnotation(Generatable.class); + + if (annotation != null && annotation.implementsFor().length > 0) { + return Arrays.stream(annotation.implementsFor()) + .map(target -> Map.entry(target, currentClass)); + } + + return Arrays.stream(currentClass.getInterfaces()) + .map(currentInterface -> Map.entry(currentInterface, currentClass)); + }) + .forEach(entry -> register(entry.getKey(), entry.getValue())); + } + + private void register(Class currentInterface, Class implementedClass) { + interfaceToImplementations + .computeIfAbsent(currentInterface, k -> new ArrayList<>()) + .add(implementedClass); + } +} diff --git a/src/main/java/org/example/generator/InstanceGenerator.java b/src/main/java/org/example/generator/InstanceGenerator.java new file mode 100644 index 0000000..a759388 --- /dev/null +++ b/src/main/java/org/example/generator/InstanceGenerator.java @@ -0,0 +1,184 @@ +package org.example.generator; + +import org.example.common.RandomProvider; +import org.example.generator.annotation.Generatable; + +import java.lang.reflect.Array; +import java.lang.reflect.Constructor; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.Collections; +import java.util.Deque; +import java.util.ArrayDeque; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class InstanceGenerator { + + private final ImplementationIndex implementationIndex; + private final ThreadLocal>> constructing = ThreadLocal.withInitial(ArrayDeque::new); + + public InstanceGenerator() { + var discovered = ClassPathScanner.scanForAnnotatedClasses(); + this.implementationIndex = new ImplementationIndex(discovered); + } + + public Object generateValueOf(Class type) { + return generateValueOf(type, -1); + } + + private Object generateValueOf(Class type, int depth) { + var primitive = PrimitiveValueGenerator.getGenerator(type); + if (primitive.isPresent()) { + return primitive.get().get(); + } + + if (type.isInterface()) { + var impl = implementationIndex.randomImplementationFor(type); + if (impl.isEmpty()) + throw new IllegalArgumentException("Не найдены реализации интерфейса: " + type.getName()); + return generateSafely(impl.get(), depth); + } + + if (Modifier.isAbstract(type.getModifiers())) { + throw new IllegalArgumentException("Класс " + type.getName() + " является абстрактным, не удаётся создать экземпляр"); + } + + if (depth == 0) { + return null; + } + + var stack = constructing.get(); + if (stack.contains(type) && !Objects.equals(stack.peek(), type)) { + return null; + } + + stack.push(type); + try { + if (!type.isAnnotationPresent(Generatable.class)) { + throw new IllegalArgumentException("Класс " + type.getName() + " нельзя сгенерировать. Требуется аннотация @CustomClassGenerator"); + } + + var constructors = type.getDeclaredConstructors(); + if (constructors.length == 0) throw new IllegalArgumentException("Класс " + type.getName() + " не имеет конструкторов"); + + Constructor constructor = constructors[RandomProvider.RANDOM.nextInt(constructors.length)]; + + int allowedForChildren = depth > 0 ? depth - 1 : -1; + var args = buildArgsForConstructor(constructor, allowedForChildren); + try { + return type.cast(constructor.newInstance(args)); + } catch (Throwable e) { + throw new RuntimeException("Не удалось создать экземпляр " + type.getName(), e); + } + } finally { + stack.pop(); + if (stack.isEmpty()) constructing.remove(); + } + } + + private Object generateSafely(Class currentClass, int depth) { + try { + return generateValueOf(currentClass, depth); + } catch (Exception e) { + throw new RuntimeException("Не удается создать значение для " + currentClass.getName(), e); + } + } + + private Object generateSafely(Class currentClass) { + return generateSafely(currentClass, -1); + } + + private Object[] buildArgsForConstructor(Constructor constructor, int allowedSelfDepthForChildren) { + var parameters = constructor.getParameters(); + var args = new Object[parameters.length]; + Class declaringClass = constructor.getDeclaringClass(); + + final Integer[] perConstructorSelfDepth = new Integer[] { + allowedSelfDepthForChildren >= 0 ? allowedSelfDepthForChildren : null + }; + + IntStream.range(0, parameters.length).forEach(i -> { + var parameter = parameters[i]; + var parameterParameterizedType = parameter.getParameterizedType(); + var parameterType = parameter.getType(); + + if (parameterParameterizedType instanceof ParameterizedType parameterizedType) { + var rawType = parameterizedType.getRawType(); + + if (rawType instanceof Class rawClass && Collection.class.isAssignableFrom(rawClass)) { + var actualTypeArguments = parameterizedType.getActualTypeArguments(); + + if (actualTypeArguments.length == 1) { + var elementClass = resolveClassFromType(actualTypeArguments[0]); + if (elementClass == null) { + args[i] = Collections.emptyList(); + return; + } + + var size = 1 + RandomProvider.RANDOM.nextInt(3); + var created = IntStream.range(0, size) + .mapToObj(j -> generateSafely(elementClass)) + .collect(Collectors.toList()); + args[i] = created; + return; + } + + args[i] = Collections.emptyList(); + return; + } + } + + if (parameterType.isArray()) { + var componentType = parameterType.getComponentType(); + var len = 1 + RandomProvider.RANDOM.nextInt(3); + var array = Array.newInstance(componentType, len); + + IntStream.range(0, len) + .forEach(j -> Array.set(array, j, generateSafely(componentType))); + args[i] = array; + return; + } + + if (parameterType.isInterface()) { + var impl = implementationIndex.randomImplementationFor(parameterType); + args[i] = impl.map(this::generateSafely).orElse(null); + return; + } + + if (parameterType.equals(declaringClass)) { + if (perConstructorSelfDepth[0] == null) { + perConstructorSelfDepth[0] = 1 + RandomProvider.RANDOM.nextInt(3); + } + int depthToPass = perConstructorSelfDepth[0]; + args[i] = generateSafely(parameterType, depthToPass); + return; + } + + args[i] = generateSafely(parameterType); + }); + + return args; + } + + private Class resolveClassFromType(Type type) { + if (type instanceof Class) return (Class) type; + + if (type instanceof ParameterizedType) { + Type raw = ((ParameterizedType) type).getRawType(); + if (raw instanceof Class) return (Class) raw; + } + + if (type instanceof GenericArrayType) { + var genericComponentType = ((GenericArrayType) type).getGenericComponentType(); + var classFromType = resolveClassFromType(genericComponentType); + if (classFromType != null) return Array.newInstance(classFromType, 0).getClass(); + } + + return null; + } +} \ No newline at end of file diff --git a/src/main/java/org/example/generator/PrimitiveValueGenerator.java b/src/main/java/org/example/generator/PrimitiveValueGenerator.java new file mode 100644 index 0000000..cbfcc0d --- /dev/null +++ b/src/main/java/org/example/generator/PrimitiveValueGenerator.java @@ -0,0 +1,38 @@ +package org.example.generator; + +import org.example.common.RandomProvider; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.function.Supplier; + +public class PrimitiveValueGenerator { + + private static final Map, Supplier> DEFAULTS = new HashMap<>(); + + static { + DEFAULTS.put(int.class, () -> RandomProvider.RANDOM.nextInt(100)); + DEFAULTS.put(Integer.class, () -> RandomProvider.RANDOM.nextInt(100)); + DEFAULTS.put(long.class, RandomProvider.RANDOM::nextLong); + DEFAULTS.put(Long.class, RandomProvider.RANDOM::nextLong); + DEFAULTS.put(double.class, RandomProvider.RANDOM::nextDouble); + DEFAULTS.put(Double.class, RandomProvider.RANDOM::nextDouble); + DEFAULTS.put(float.class, RandomProvider.RANDOM::nextFloat); + DEFAULTS.put(Float.class, RandomProvider.RANDOM::nextFloat); + DEFAULTS.put(boolean.class, RandomProvider.RANDOM::nextBoolean); + DEFAULTS.put(Boolean.class, RandomProvider.RANDOM::nextBoolean); + DEFAULTS.put(char.class, () -> (char) (RandomProvider.RANDOM.nextInt(26) + 'a')); + DEFAULTS.put(Character.class, () -> (char) (RandomProvider.RANDOM.nextInt(26) + 'a')); + DEFAULTS.put(byte.class, () -> (byte) RandomProvider.RANDOM.nextInt()); + DEFAULTS.put(Byte.class, () -> (byte) RandomProvider.RANDOM.nextInt()); + DEFAULTS.put(short.class, () -> (short) RandomProvider.RANDOM.nextInt(Short.MAX_VALUE + 1)); + DEFAULTS.put(Short.class, () -> (short) RandomProvider.RANDOM.nextInt(Short.MAX_VALUE + 1)); + DEFAULTS.put(String.class, () -> UUID.randomUUID().toString().substring(0, 8)); + } + + static Optional> getGenerator(Class type) { + return Optional.ofNullable(DEFAULTS.get(type)); + } +} diff --git a/src/main/java/org/example/generator/annotation/Generatable.java b/src/main/java/org/example/generator/annotation/Generatable.java new file mode 100644 index 0000000..44f876a --- /dev/null +++ b/src/main/java/org/example/generator/annotation/Generatable.java @@ -0,0 +1,9 @@ +package org.example.generator.annotation; + +import java.lang.annotation.*; + +@Target(value = ElementType.TYPE) +@Retention(value = RetentionPolicy.RUNTIME) +public @interface Generatable { + Class[] implementsFor() default {}; +}