generated from JavaLabs2025/reflection-template
-
Notifications
You must be signed in to change notification settings - Fork 34
LAB-2 class generator solution #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
quicklybly
wants to merge
14
commits into
JavaLabs2025:review1
Choose a base branch
from
JavaLabs2025:master
base: review1
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
f4e385f
add deadline
github-classroom[bot] 2941ba9
setup project structure
cc5928e
add primitives support
073d313
update tests
12eb283
add generation of simple types and enums
79b4929
add generation of array
36d1dbb
add generation collection and map
f369b96
extract test enums to files
409af57
fix maxDepth behavior
c2c3b0f
add generation of internal collections and maps
41282cc
add package for scanInApi
253dd97
add PackageUtils
fad8ac6
add interface generation
c6952f8
update types
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 { | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package org.example.generator; | ||
|
|
||
| public class GenerationException extends Exception { | ||
| public GenerationException(String message) { | ||
| super(message); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<Class<?>, Supplier<?>> generators; | ||
|
|
||
| private final int maxDepth; | ||
| private final String packageToScan; | ||
| private final Set<Class<?>> classesInPackageToScan; | ||
|
|
||
| private final Random random = new Random(); | ||
|
|
||
| public Generator( | ||
| Collection<TypeGeneratorsProvider> providers, | ||
| int maxDepth, | ||
| Object packageMarker | ||
| ) { | ||
| Map<Class<?>, Supplier<?>> result = new HashMap<>(); | ||
|
|
||
| for (TypeGeneratorsProvider provider : providers) { | ||
| Map<Class<?>, Supplier<?>> generatorsFromProvider = provider.getGenerators(); | ||
|
|
||
| for (Map.Entry<Class<?>, 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<Object> 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<Object, Object> generateMapFromClass(Class<?> mapClass) { | ||
| if (SortedMap.class.isAssignableFrom(mapClass)) { | ||
| return new TreeMap<>(); | ||
| } | ||
| return new HashMap<>(); | ||
| } | ||
|
|
||
| private Optional<Class<?>> findImplementationClass(Class<?> interfaceClass) { | ||
| if (!interfaceClass.getPackageName().startsWith(packageToScan)) { | ||
| return Optional.empty(); | ||
| } | ||
|
|
||
| List<Class<?>> 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<Object> 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<Object, Object> 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; | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Напомните пж, почему мы тут не делаем также, как и в fromField варианте:
if (genericType instanceof ParameterizedType parameterizedTypeThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
В рантайме тип generic'а стирается
Демонстрация:
Попробуем воспроизвести этот код в метода
generateCollectionFromClassдляCart.javaТеперь посмотрим на Field
Класс знает, что тип поля не изменится и может сохранить значение generic'а у себя в метаинформации, на скрине видно, что эта информация легко доступна