typesToMatch) {
+ return typesToMatch.stream()
+ .map(H09_TestUtils::match)
+ .reduce(Predicate::or)
+ .orElse(new H09_TestUtils.GenericPredicate(i -> false, "Expected type is not defined"));
+ }
+
+ /**
+ * This method returns the upper and lower bounds of the given type.
+ *
+ * The returned Pair contains a list of lower bounds in the left Parameter and a list of upper bounds in the right
+ * Parameter.
+ *
+ *
If the given Type does not have any lower bounds the left element of the Pair will be null.
+ *
+ *
If the given Type is not generic this method will return null.
+ *
+ * @param type the type to getrieve the Bounds from
+ * @return a Pair containing both the upper and the lower bounds of the given Type
+ */
+ public static Pair, List> getBounds(Type type) {
+ if (type instanceof WildcardType wildcardType) {
+ return ImmutablePair.of(Arrays.asList(wildcardType.getLowerBounds()), Arrays.asList(wildcardType.getUpperBounds()));
+ }
+ if (type instanceof ParameterizedType parameterizedType) {
+ return ImmutablePair.of(null, Arrays.asList(parameterizedType.getActualTypeArguments()));
+ }
+ if (type instanceof TypeVariable> typeVariable) {
+ return ImmutablePair.of(null, Arrays.asList(typeVariable.getBounds()));
+ }
+ if (type instanceof GenericArrayType) {
+ return ImmutablePair.of(null, List.of(Object[].class));
+ }
+ return null;
+ }
+
+ /**
+ * Retrieves the inner type of the given Type.
+ *
+ * @param type the type to get the inner type from.
+ * @return the inner type of the given type. Returns empty list if no inner type is present.
+ */
+ public static List getInnerTypes(Type type) {
+ if (!(type instanceof ParameterizedType parameterizedType)) {
+ return List.of();
+ }
+ return Arrays.asList(parameterizedType.getActualTypeArguments());
+ }
+
+ /**
+ * Retrieves the super Types of the given Type if it is a class. Returns upper bounds otherwise.
+ *
+ * @param type the type to get the super types from.
+ * @return the super types or upper bounds of the given type. Returns a list containing only Object if no supertype can be
+ * determined.
+ */
+ public static List getGenericSuperTypes(Type type) {
+ if (type instanceof Class> clazz) {
+ List superTypes = new ArrayList<>();
+ if (clazz.getGenericSuperclass() != null) {
+ superTypes.add(clazz.getGenericSuperclass());
+ }
+ if (clazz.getGenericInterfaces().length > 0) {
+ superTypes.addAll(List.of(clazz.getGenericInterfaces()));
+ }
+ return superTypes;
+ }
+
+ Pair, List> bounds = getBounds(type);
+ if (bounds != null) {
+ return bounds.getRight();
+ }
+ return List.of(Object.class);
+ }
+
+ /**
+ * Retrieves the return type of the given Method.
+ *
+ * @param method the method to get the return type from.
+ * @return the return type of the given method.
+ */
+ public static Type getReturnType(Method method) {
+ return method.getGenericReturnType();
+ }
+
+ /**
+ * Retrieves all parameters of the given method. Returned parameters may not generic.
+ *
+ * @param method the method that the generic types should be retrieved from.
+ * @param regex a regex that is used to filter all generic type names.
+ * @return a List containing all types of the parameters from the method whose type names match the given regex.
+ */
+ public static List getTypeParameters(Method method, String regex) {
+ return Arrays.stream(method.getGenericParameterTypes()).filter(t -> t.getTypeName().matches(regex)).toList();
+ }
+
+ /**
+ * Retrieves all generic types that are defined by the given method.
+ *
+ * @param method the method that the generic types should be retrieved from.
+ * @param regex a regex that is used to filter all generic type names.
+ * @return a List containing all defined types that match the given regex.
+ */
+ public static List getDefinedTypes(Method method, String regex) {
+ return Arrays.stream(method.getTypeParameters()).filter(t -> t.getTypeName().matches(regex)).map(t -> (Type) t).toList();
+ }
+
+ /**
+ * Retrieves all generic types that are defined by the given class.
+ *
+ * @param clazz the class that the generic types should be retrieved from.
+ * @param regex a regex that is used to filter all generic type names.
+ * @return a List containing all defined types that match the given regex.
+ */
+ public static List getDefinedTypes(Class clazz, String regex) {
+ return Arrays.stream(clazz.getTypeParameters()).filter(t -> t.getTypeName().matches(regex)).map(t -> (Type) t).toList();
+ }
+
+ /**
+ * Asserts that the given {@link Class} defines a certain set a generic Parameters.
+ *
+ * @param clazz the Class that should be tested.
+ * @param expected a set of predicates that is used to check if all defined generic Types match an expected Type.
+ */
+ public static void assertDefinedParameters(Class> clazz, Set> expected) {
+
+ List> typeVariable = Arrays.asList(clazz.getTypeParameters());
+ CtType> ctClass = (CtType>) BasicTypeLink.of(clazz).getCtElement();
+ var actualNames =
+ ctClass.getFormalCtTypeParameters().stream().map(CtType::toStringDebug).map(s -> s.replace("\n", "")).toList();
+ Context context = contextBuilder()
+ .add("expected", expected)
+ .add("actual", actualNames)
+ .build();
+
+ assertTrue(
+ !typeVariable.isEmpty(),
+ emptyContext(),
+ r -> clazz.getSimpleName() + " does not have any generic parameters."
+ );
+
+ assertEquals(
+ expected.size(),
+ typeVariable.size(),
+ context,
+ r -> clazz.getSimpleName() + " does not have the expected number of generic parameters."
+ );
+ typeVariable.forEach(a ->
+ assertTrue(
+ expected.stream().anyMatch(e -> e.test(a)),
+ context,
+ r -> String.format("The type parameter %s of %s do not match any expected types.", a, clazz.getSimpleName())
+ )
+ );
+ }
+
+ /**
+ * Asserts that the given {@link Method} defines a specific set of generic types.
+ *
+ * @param method the method that is checked for type definitions.
+ * @param expected a set of predicates that is used to check if all defined generic Types match an expected Type.
+ */
+ public static void assertDefinedParameters(Method method, Set> expected) {
+
+ List> typeVariable = Arrays.asList(method.getTypeParameters());
+ CtMethod> ctMethod = BasicMethodLink.of(method).getCtElement();
+ var actualNames = ctMethod.getFormalCtTypeParameters()
+ .stream()
+ .map(CtTypeParameter::toStringDebug)
+ .map(s -> s.replace("\n", ""))
+ .toList();
+ Context context = contextBuilder()
+ .add("expected", expected)
+ .add("actual", actualNames)
+ .build();
+
+ assertTrue(!typeVariable.isEmpty(), emptyContext(), r -> method.getName() + " does not have any generic parameters.");
+
+ assertEquals(
+ expected.size(),
+ typeVariable.size(),
+ context,
+ r -> method.getName() + " does not have the expected number of generic parameters."
+ );
+ typeVariable.forEach(a ->
+ assertTrue(
+ expected.stream().anyMatch(e -> e.test(a)),
+ context,
+ r -> String.format("The type parameter %s of %s do not match any expected types.", a, method.getName())
+ )
+ );
+ }
+
+ /**
+ * Asserts that the given {@link Method} has a return type that matches the given {@link Predicate}.
+ *
+ * @param method the method that should be tested.
+ * @param expected the {@link Predicate} that shoul be used to check the return type.
+ */
+ public static void assertReturnParameter(Method method, Predicate expected) {
+ Type type = method.getGenericReturnType();
+
+ CtMethod> ctMethod = BasicMethodLink.of(method).getCtElement();
+ var actualNames = ctMethod.getType().toStringDebug().replace("\n", "");
+ Context context = contextBuilder()
+ .add("actual type", actualNames)
+ .add("expected", expected)
+ .build();
+
+ assertTrue(expected.test(type), context, r -> String.format("%s has a wrong return type.", method.getName()));
+ }
+
+ /**
+ * Asserts that the given {@link Method} has a correct list of parameters each parameter is checked with the given
+ * {@link Predicate} for the index.
+ *
+ * @param method the method that should be checked
+ * @param expected a list containing a {@link Predicate} for each Parameter of the method.
+ */
+ public static void assertParameters(Method method, List> expected) {
+ Type[] type = method.getGenericParameterTypes();
+
+ assertEquals(
+ expected.size(),
+ type.length,
+ emptyContext(), r -> String.format("The method %s() does not have the correct amount of parameters", method.getName())
+ );
+
+ CtMethod> ctMethod = BasicMethodLink.of(method).getCtElement();
+ var actualNames =
+ ctMethod.getParameters()
+ .stream()
+ .map(CtParameter::getType)
+ .map(CtTypeReference::toStringDebug)
+ .map(s -> s.replace("\n", ""))
+ .toList();
+
+ for (int i = 0; i < type.length; i++) {
+ int finalI = i;
+
+ Context context = contextBuilder()
+ .add("actual type", actualNames.get(i))
+ .add("expected", expected.get(i))
+ .build();
+
+ assertTrue(
+ expected.get(i).test(type[i]),
+ context,
+ r -> String.format("%s has a wrong parameter at index %d.", method.getName(), finalI)
+ );
+ }
+
+ }
+
+ /**
+ * Asserts that the given field has a {@link Type} that matches the given {@link Predicate}.
+ *
+ * @param field the field that should be checked.
+ * @param expected the {@link Predicate} that is used to check if the Field has a correct Type.
+ */
+ public static void assertType(Field field, Predicate expected) {
+ Type type = field.getGenericType();
+
+ CtField> ctField =
+ BasicTypeLink.of(field.getDeclaringClass()).getCtElement().filterChildren(new TypeFilter<>(CtField.class) {
+ @Override
+ public boolean matches(CtField element) {
+ return super.matches(element) && element.getSimpleName().equals(field.getName());
+ }
+ }).first();
+ var actualNames = ctField.getType().toStringDebug();
+
+ Context context = contextBuilder()
+ .add("actual type", actualNames)
+ .add("expected", expected)
+ .build();
+
+ assertTrue(expected.test(type), context, r -> String.format("%s has a wrong type.", field.getName()));
+
+ }
+
+ /**
+ * Asserts that the given method is generic.
+ *
+ * @param toTest a method reference to the method that should be checked.
+ */
+ public static void assertGeneric(Method toTest) {
+
+ Predicate isGeneric = (method) -> !getTypeParameters(method, ".*").isEmpty();
+ isGeneric = isGeneric.or((method) -> getBounds(getReturnType(method)) != null);
+
+ assertTrue(
+ isGeneric.test(toTest),
+ emptyContext(),
+ r -> String.format("The method %s() is not Generic.", toTest.getName())
+ );
+ }
+
+ /**
+ * A simple Predicate that can store a custom toString() method for better readability.
+ */
+ public static class GenericPredicate implements Predicate {
+
+ /**
+ * The description that should be displayed if toString() is called.
+ */
+ private final String description;
+ /**
+ * The underlying predicate.
+ */
+ private final Predicate predicate;
+
+ /**
+ * Creates a new {@link GenericPredicate} from a {@link Predicate} and a short description that describes what the
+ * predicate matches.
+ *
+ * @param predicate the predicate that should be used to match any object.
+ * @param description the description of what the predicate matches.
+ */
+ GenericPredicate(Predicate predicate, String description) {
+ this.predicate = predicate;
+ this.description = description;
+ }
+
+ @Override
+ public boolean test(Type type) {
+ return predicate.test(type);
+ }
+
+ @NotNull
+ @Override
+ public Predicate and(@NotNull Predicate super Type> other) {
+ return new GenericPredicate(predicate.and(other), "(" + this.description + " and " + other + ")");
+ }
+
+ @NotNull
+ @Override
+ public Predicate negate() {
+ return new GenericPredicate(predicate.negate(), "(not " + description + ")");
+ }
+
+ @NotNull
+ @Override
+ public Predicate or(@NotNull Predicate super Type> other) {
+ return new GenericPredicate(predicate.or(other), "(" + this.description + " or " + other + ")");
+ }
+
+ @Override
+ public String toString() {
+ return description;
+ }
+ }
+}
diff --git a/H09/src/graderPublic/java/h09/ReflectionUtils.java b/H09/src/graderPublic/java/h09/ReflectionUtils.java
new file mode 100644
index 0000000..9160c90
--- /dev/null
+++ b/H09/src/graderPublic/java/h09/ReflectionUtils.java
@@ -0,0 +1,191 @@
+package h09;
+
+import com.google.common.primitives.Primitives;
+import sun.misc.Unsafe;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Deque;
+import java.util.List;
+import java.util.Optional;
+
+public class ReflectionUtils {
+
+ public static void setFieldValue(Object instance, String fieldName, Object value) {
+ try {
+ Class> objectClass = instance.getClass();
+ Field declaredField;
+ try {
+ declaredField = objectClass.getDeclaredField(fieldName);
+ } catch (NoSuchFieldException e) {
+ declaredField = getSuperClassesIncludingSelf(objectClass).stream()
+ .filter((c) -> List.of(c.getDeclaredFields()).stream()
+ .map(Field::getName)
+ .anyMatch(name -> name.equals(fieldName))
+ )
+ .map((c) -> {
+ try {
+ return c.getDeclaredField(fieldName);
+ } catch (NoSuchFieldException ex) {
+ throw new RuntimeException(ex);
+ }
+ })
+ .findFirst()
+ .orElseThrow(() -> new NoSuchFieldException(e.getMessage()));
+ }
+
+ //best case field in non Final
+ if (!Modifier.isFinal(declaredField.getModifiers())) {
+ try {
+ declaredField.setAccessible(true);
+ declaredField.set(instance, value);
+ return;
+ } catch (Exception ignored) {
+ }
+ }
+
+ //field has setter
+ Optional setter = Arrays
+ .stream(objectClass.getDeclaredMethods())
+ .filter(
+ m -> m.getName().equalsIgnoreCase("set" + fieldName)
+ ).findFirst();
+ if (setter.isPresent()) {
+ setter.get().invoke(instance, value);
+ return;
+ }
+
+ //rely on Unsafe to set value
+ Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
+ unsafeField.setAccessible(true);
+ Unsafe unsafe = (Unsafe) unsafeField.get(null);
+
+ Field theInternalUnsafeField = Unsafe.class.getDeclaredField("theInternalUnsafe");
+ theInternalUnsafeField.setAccessible(true);
+ Object theInternalUnsafe = theInternalUnsafeField.get(null);
+
+ Method offset = Class.forName("jdk.internal.misc.Unsafe").getMethod("objectFieldOffset", Field.class);
+ unsafe.putBoolean(offset, 12, true);
+
+ switch (value) {
+ case Boolean val -> unsafe.putBoolean(instance, (long) offset.invoke(theInternalUnsafe, declaredField), val);
+ case Character val -> unsafe.putChar(instance, (long) offset.invoke(theInternalUnsafe, declaredField), val);
+ case Short val -> unsafe.putShort(instance, (long) offset.invoke(theInternalUnsafe, declaredField), val);
+ case Integer val -> unsafe.putInt(instance, (long) offset.invoke(theInternalUnsafe, declaredField), val);
+ case Long val -> unsafe.putLong(instance, (long) offset.invoke(theInternalUnsafe, declaredField), val);
+ case Double val -> unsafe.putDouble(instance, (long) offset.invoke(theInternalUnsafe, declaredField), val);
+ case Float val -> unsafe.putFloat(instance, (long) offset.invoke(theInternalUnsafe, declaredField), val);
+ default -> unsafe.putObject(instance, (long) offset.invoke(theInternalUnsafe, declaredField), value);
+ }
+ } catch (IllegalAccessException | NoSuchFieldException | ClassNotFoundException | NoSuchMethodException |
+ InvocationTargetException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public static T getFieldValue(Object instance, String fieldName) {
+ Field f;
+ Class> fieldType = null;
+ try {
+ f = instance.getClass().getDeclaredField(fieldName);
+
+ try {
+ f.setAccessible(true);
+ return (T) f.get(instance);
+ } catch (Exception ignored) {
+ }
+
+ fieldType = f.getType();
+ if (Primitives.isWrapperType(fieldType)) {
+ fieldType = Primitives.unwrap(fieldType);
+ }
+
+ Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
+ unsafeField.setAccessible(true);
+ Unsafe unsafe = (Unsafe) unsafeField.get(null);
+
+ Field theInternalUnsafeField = Unsafe.class.getDeclaredField("theInternalUnsafe");
+ theInternalUnsafeField.setAccessible(true);
+ Object theInternalUnsafe = theInternalUnsafeField.get(null);
+
+ Method offset = Class.forName("jdk.internal.misc.Unsafe").getMethod("objectFieldOffset", Field.class);
+ unsafe.putBoolean(offset, 12, true);
+
+ Object fieldValue;
+ if (boolean.class == fieldType) {
+ fieldValue = unsafe.getBoolean(instance, (long) offset.invoke(theInternalUnsafe, f));
+ } else if (byte.class == fieldType) {
+ fieldValue = unsafe.getByte(instance, (long) offset.invoke(theInternalUnsafe, f));
+ } else if (short.class == fieldType) {
+ fieldValue = unsafe.getShort(instance, (long) offset.invoke(theInternalUnsafe, f));
+ } else if (int.class == fieldType) {
+ fieldValue = unsafe.getInt(instance, (long) offset.invoke(theInternalUnsafe, f));
+ } else if (long.class == fieldType) {
+ fieldValue = unsafe.getLong(instance, (long) offset.invoke(theInternalUnsafe, f));
+ } else if (float.class == fieldType) {
+ fieldValue = unsafe.getFloat(instance, (long) offset.invoke(theInternalUnsafe, f));
+ } else if (double.class == fieldType) {
+ fieldValue = unsafe.getDouble(instance, (long) offset.invoke(theInternalUnsafe, f));
+ } else if (char.class == fieldType) {
+ fieldValue = unsafe.getChar(instance, (long) offset.invoke(theInternalUnsafe, f));
+ } else {
+ fieldValue = unsafe.getObject(instance, (long) offset.invoke(theInternalUnsafe, f));
+ }
+ return (T) fieldValue;
+ } catch (NoSuchFieldException | ClassNotFoundException | NoSuchMethodException | InvocationTargetException |
+ IllegalAccessException e) {
+ throw new RuntimeException(
+ "Could not set value for Field %s(%s) in %s. Please do not access this field.".formatted(
+ fieldName,
+ fieldType,
+ instance.getClass()
+ ), e
+ );
+ }
+ }
+
+ public static void copyFields(Object source, Object dest) {
+ for (Field f : source.getClass().getDeclaredFields()) {
+ setFieldValue(dest, f.getName(), getFieldValue(source, f.getName()));
+ }
+ }
+
+ public static boolean actsLikePrimitive(Class> type) {
+ return type.isPrimitive() ||
+ Enum.class.isAssignableFrom(type) ||
+ Primitives.isWrapperType(type) ||
+ type == String.class;
+ }
+
+ public static List> getSuperClassesIncludingSelf(Class> clazz) {
+ List> classes = new ArrayList<>();
+ Deque> classDeque = new ArrayDeque<>();
+
+ classDeque.add(clazz);
+
+ while ((clazz = classDeque.peekFirst()) != null) {
+ classDeque.pop();
+
+ classes.add(clazz);
+ if (clazz.getSuperclass() != null) {
+ classDeque.add(clazz.getSuperclass());
+ }
+ if (clazz.getInterfaces().length > 0) {
+ classDeque.addAll(List.of(clazz.getInterfaces()));
+ }
+
+ }
+ return classes;
+ }
+
+ public static boolean isObjectMethod(Method methodToCheck) {
+ List objectMethods =
+ List.of("getClass", "hashCode", "equals", "clone", "toString", "notify", "notifyAll", "wait", "finalize");
+ return objectMethods.contains(methodToCheck.getName());
+ }
+}
diff --git a/H09/src/graderPublic/java/h09/StackOfObjectsTestPublic.java b/H09/src/graderPublic/java/h09/StackOfObjectsTestPublic.java
new file mode 100644
index 0000000..8dee2e6
--- /dev/null
+++ b/H09/src/graderPublic/java/h09/StackOfObjectsTestPublic.java
@@ -0,0 +1,101 @@
+package h09;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.sourcegrade.jagr.api.rubric.TestForSubmission;
+import org.tudalgo.algoutils.tutor.general.reflections.BasicTypeLink;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.GenericArrayType;
+import java.lang.reflect.Method;
+import java.lang.reflect.Type;
+import java.util.List;
+import java.util.Set;
+
+import static h09.H09_TestUtils.assertDefinedParameters;
+import static h09.H09_TestUtils.assertParameters;
+import static h09.H09_TestUtils.assertReturnParameter;
+import static h09.H09_TestUtils.assertType;
+import static h09.H09_TestUtils.getTypeParameters;
+import static h09.H09_TestUtils.match;
+import static h09.H09_TestUtils.matchArray;
+import static h09.H09_TestUtils.matchNested;
+import static h09.H09_TestUtils.matchNoBounds;
+import static org.tudalgo.algoutils.tutor.general.assertions.Assertions2.assertNotNull;
+import static org.tudalgo.algoutils.tutor.general.assertions.Assertions2.emptyContext;
+import static org.tudalgo.algoutils.tutor.general.match.BasicStringMatchers.identical;
+
+@TestForSubmission
+public class StackOfObjectsTestPublic {
+
+ BasicTypeLink stackLink;
+ Class> ctClassStack;
+ Method get;
+ Method pop;
+ Method push;
+ Method remove;
+ Method of;
+ Field objs;
+
+
+ @BeforeEach
+ public void setUp() {
+ stackLink = BasicTypeLink.of(StackOfObjects.class);
+ ctClassStack = stackLink.reflection();
+ get = stackLink.getMethod(identical("get")).reflection();
+ pop = stackLink.getMethod(identical("pop")).reflection();
+ push = stackLink.getMethod(identical("push")).reflection();
+ remove = stackLink.getMethod(identical("remove")).reflection();
+ of = stackLink.getMethod(identical("of")).reflection();
+ objs = stackLink.getField(identical("objs")).reflection();
+ }
+
+ @Test
+ public void testClassParameter() {
+ assertDefinedParameters(ctClassStack, Set.of(matchNoBounds("O")));
+ }
+
+ @Test
+ public void testObjsType() {
+ assertType(objs, matchArray(matchNoBounds("O")));
+ assertNotNull(
+ ReflectionUtils.getFieldValue(new StackOfObjects<>(), "objs"),
+ emptyContext(),
+ r -> "Field objs is not correctly initialized"
+ );
+ }
+
+ @Test
+ public void testPushParameter() {
+ assertParameters(push, List.of(matchNoBounds("O")));
+ }
+
+ @Test
+ public void testRemoveParameter() {
+ assertParameters(remove, List.of(matchNoBounds("O")));
+ }
+
+ @Test
+ public void testPopParameter() {
+ assertReturnParameter(pop, matchNoBounds("O"));
+ }
+
+ @Test
+ public void testGetParameter() {
+ assertReturnParameter(get, matchNoBounds("O"));
+ }
+
+ @Test
+ public void testOfParameter() {
+ assertDefinedParameters(of, Set.of(matchNoBounds("O")));
+
+ List types = getTypeParameters(of, ".*");
+
+ assertReturnParameter(
+ of,
+ matchNested(StackOfObjects.class, match(((GenericArrayType) types.get(0)).getGenericComponentType()))
+ );
+
+ assertParameters(of, List.of(match(types.get(0))));
+ }
+}
diff --git a/H09/src/graderPublic/java/h09/WaterEnclosureTestPublic.java b/H09/src/graderPublic/java/h09/WaterEnclosureTestPublic.java
new file mode 100644
index 0000000..deccde2
--- /dev/null
+++ b/H09/src/graderPublic/java/h09/WaterEnclosureTestPublic.java
@@ -0,0 +1,140 @@
+package h09;
+
+import h09.abilities.Swims;
+import h09.animals.Penguin;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.sourcegrade.jagr.api.rubric.TestForSubmission;
+import org.tudalgo.algoutils.tutor.general.assertions.Context;
+import org.tudalgo.algoutils.tutor.general.reflections.BasicTypeLink;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Stream;
+
+import static org.mockito.Mockito.CALLS_REAL_METHODS;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.tudalgo.algoutils.tutor.general.assertions.Assertions2.assertTrue;
+import static org.tudalgo.algoutils.tutor.general.assertions.Assertions2.contextBuilder;
+import static org.tudalgo.algoutils.tutor.general.assertions.Assertions2.fail;
+import static org.tudalgo.algoutils.tutor.general.match.BasicStringMatchers.identical;
+
+@TestForSubmission
+public class WaterEnclosureTestPublic {
+
+ BasicTypeLink waterEnclosureLink;
+ Class> waterEnclosureClass;
+ Method getStack;
+ Method feed;
+ Method getMeanElevation;
+ Field animals;
+
+
+ @BeforeEach
+ public void setUp() {
+ waterEnclosureLink = BasicTypeLink.of(WaterEnclosure.class);
+ waterEnclosureClass = waterEnclosureLink.reflection();
+ getStack = waterEnclosureLink.getMethod(identical("getStack")).reflection();
+ feed = waterEnclosureLink.getMethod(identical("feed")).reflection();
+ getMeanElevation = waterEnclosureLink.getMethod(identical("getMeanElevation")).reflection();
+ animals = waterEnclosureLink.getField(identical("animals")).reflection();
+ }
+
+ @Test
+ public void testFeed() {
+
+ WaterEnclosure enclosure = new WaterEnclosure();
+ List animals = new ArrayList<>();
+
+ for (int i = 0; i < 10; i++) {
+ Penguin mock = mock(Penguin.class, CALLS_REAL_METHODS);
+ ReflectionUtils.setFieldValue(mock, "isHungry", false);
+ ReflectionUtils.setFieldValue(mock, "elevation", Swims.MAX_ELEVATION);
+ if (i % 2 == 0) {
+ ReflectionUtils.setFieldValue(mock, "isHungry", true);
+ }
+ if (i % 3 == 0) {
+ ReflectionUtils.setFieldValue(mock, "elevation", Swims.MIN_ELEVATION);
+ }
+
+ enclosure.getStack().push(mock);
+ animals.add(mock);
+ }
+
+ Context context = contextBuilder()
+ .add("Animals", animals)
+ .build();
+
+ enclosure.feed();
+
+ for (int i = 0; i < 10; i++) {
+ if (enclosure.getStack().size() <= 0) {
+ fail(context, r -> "WaterEnclosure does not have correct number of Animals after feeding.");
+ }
+ Penguin mock = (Penguin) enclosure.getStack().pop();
+
+ int id = animals.indexOf(mock);
+
+ if (id % 2 == 0) {
+ verify(mock).eat();
+ verify(mock).swimDown();
+ if (i % 3 == 0) {
+ verify(mock).swimUp();
+ } else {
+ verify(mock, never()).swimUp();
+ }
+ } else {
+ verify(mock, never()).eat();
+ }
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("provide_testGetMeanElevation")
+ public void testGetMeanElevation(List elevations, double expected) {
+
+ WaterEnclosure enclosure = new WaterEnclosure<>();
+
+ for (double elevation : elevations) {
+ Penguin mock = mock(Penguin.class);
+ when(mock.getElevation()).thenReturn((float) elevation);
+
+ enclosure.getStack().push(mock);
+ }
+
+ float actual = enclosure.getMeanElevation();
+
+ Context context = contextBuilder()
+ .add("Elevations", elevations)
+ .build();
+
+ double error = 0.00001;
+
+ assertTrue(
+ Math.abs(Math.abs(expected) - Math.abs(actual)) < error,
+ context,
+ r -> "Average Elevation is not calculated correctly."
+ );
+ }
+
+ public static Stream provide_testGetMeanElevation() {
+ return Stream.of(
+ Arguments.of(List.of(0d, 0d, 0d, 0d, 0d, 0d), 0.0),
+ Arguments.of(List.of(0d, -1d, -2d, -3d, -4d, -5d), -2.5),
+ Arguments.of(List.of(-1d, -2d, -3d, -4d, -5d, -6d), -3.5),
+ Arguments.of(List.of(-6d, -5d, -4d, -3d, -2d, -1d), -3.5),
+ Arguments.of(List.of(-3d, -5d, 0d, -3d, -10d, -1d), -3.6666667),
+ Arguments.of(List.of(-3d, -2d, 0d, -3d, -2d, -3d), -2.1666667),
+ Arguments.of(List.of(-2d, -2d, -2d, -2d, -2d, -2d), -2.0),
+ Arguments.of(List.of(-1d, -2d, -2d), -1.6666666)
+ );
+ }
+}
diff --git a/H09/src/main/java/h09/Enclosure.java b/H09/src/main/java/h09/Enclosure.java
new file mode 100644
index 0000000..561255d
--- /dev/null
+++ b/H09/src/main/java/h09/Enclosure.java
@@ -0,0 +1,119 @@
+package h09;
+
+import h09.animals.Animal;
+import org.tudalgo.algoutils.student.annotation.DoNotTouch;
+import org.tudalgo.algoutils.student.annotation.StudentImplementationRequired;
+
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+
+/**
+ * An object of a class implementing {@link Enclosure} has the ability to contain and manage a stack of {@link Animal}s.
+ */
+// TODO: H9.2.1
+public interface Enclosure {
+ /**
+ * @return the stack of animals which is used manage the contained {@link Animal}s
+ */
+ @StudentImplementationRequired("H9.2.1")
+ // TODO: H9.2.1
+ StackOfObjects getStack();
+
+ /**
+ * Feeds all contained animals.
+ */
+ @DoNotTouch
+ void feed();
+
+ /**
+ * Counts the number of hungry {@link Animal}s in the enclosure.
+ *
+ * @return number of hungry {@link Animal}s in the enclosure
+ */
+ @DoNotTouch
+ @SuppressWarnings("RedundantCast")
+ default int countHungry() {
+ int count = 0;
+ for (int i = 0; i < this.getStack().size(); i++)
+ if (((Animal) this.getStack().get(i)).isHungry()) count++;
+ return count;
+ }
+
+
+ /**
+ * Applies a {@link Consumer} operation on each {@link Animal} in the enclosure.
+ *
+ * @param func operation to be applied to each {@link Animal} in the enclosure
+ */
+ @StudentImplementationRequired("H9.3.1") // TODO: H9.3.1
+ default void forEach(Consumer func) {
+ for (int i = 0; i < this.getStack().size(); i++)
+ func.accept(this.getStack().get(i));
+ }
+
+ /**
+ * Tests a {@link Predicate} operation on each {@link Animal} in the enclosure and removes every {@link Animal}
+ * which does not satisfy the predicate. That means only {@link Animal}s for which the predicate returns 'true'
+ * remain in the enclosure.
+ *
+ * @param filter operation to test to each {@link Animal} in the enclosure
+ */
+ @StudentImplementationRequired("H9.3.2") // TODO: H9.3.2
+ default void filterObj(Predicate filter) {
+ for (int i = 0; i < this.getStack().size(); i++) {
+ Object a = this.getStack().get(i);
+ if (!filter.test(a)) {
+ this.getStack().remove(a);
+ i--;
+ }
+ }
+ }
+
+ /**
+ * Returns a new {@link Enclosure} that contains only the {@link Animal}s of the previous {@link Enclosure} which
+ * satisfied the predicate. That means only {@link Animal}s for which the predicate returns 'true' are included
+ * in the new enclosure.
+ *
+ * @param supp {@link Supplier} which is used to create the new {@link Enclosure} to be returned
+ * @param filter operation to test to each {@link Animal} in the enclosure
+ * @param Type of the new {@link Enclosure} which is returned
+ * @return a new {@link Enclosure} that contains only the {@link Animal}s of the previous {@link Enclosure} which
+ * satisfied the predicate
+ */
+ @StudentImplementationRequired("H9.3.3")
+ default Enclosure filterFunc(Supplier supp, Predicate