From c82fcb1975031db2a8da8fe07b64ae2c0896ae19 Mon Sep 17 00:00:00 2001 From: Thad House Date: Fri, 12 May 2023 21:35:39 -0700 Subject: [PATCH] [wpiutil] Add reflection based cleanup helper (#4919) Co-authored-by: Starlight220 <53231611+Starlight220@users.noreply.github.com> --- .../wpi/first/util/cleanup/CleanupPool.java | 55 +++++++ .../first/util/cleanup/ReflectionCleanup.java | 50 ++++++ .../wpi/first/util/cleanup/SkipCleanup.java | 14 ++ .../first/util/cleanup/CleanupPoolTest.java | 154 ++++++++++++++++++ .../util/cleanup/ReflectionCleanupTest.java | 61 +++++++ 5 files changed, 334 insertions(+) create mode 100644 wpiutil/src/main/java/edu/wpi/first/util/cleanup/CleanupPool.java create mode 100644 wpiutil/src/main/java/edu/wpi/first/util/cleanup/ReflectionCleanup.java create mode 100644 wpiutil/src/main/java/edu/wpi/first/util/cleanup/SkipCleanup.java create mode 100644 wpiutil/src/test/java/edu/wpi/first/util/cleanup/CleanupPoolTest.java create mode 100644 wpiutil/src/test/java/edu/wpi/first/util/cleanup/ReflectionCleanupTest.java diff --git a/wpiutil/src/main/java/edu/wpi/first/util/cleanup/CleanupPool.java b/wpiutil/src/main/java/edu/wpi/first/util/cleanup/CleanupPool.java new file mode 100644 index 0000000000..fab8316626 --- /dev/null +++ b/wpiutil/src/main/java/edu/wpi/first/util/cleanup/CleanupPool.java @@ -0,0 +1,55 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package edu.wpi.first.util.cleanup; + +import edu.wpi.first.util.ErrorMessages; +import java.util.ArrayDeque; +import java.util.Deque; + +/** + * An object containing a Stack of AutoCloseable objects that are closed when this object is closed. + */ +public class CleanupPool implements AutoCloseable { + // Use a Deque instead of a Stack, as Stack's iterators go the wrong way, and docs + // state ArrayDeque is faster anyway. + private final Deque m_closers = new ArrayDeque(); + + /** + * Registers an object in the object stack for cleanup. + * + * @param The object type + * @param object The object to register + * @return The registered object + */ + public T register(T object) { + ErrorMessages.requireNonNullParam(object, "object", "register"); + m_closers.addFirst(object); + return object; + } + + /** + * Removes an object from the cleanup stack. + * + * @param object the object to remove + */ + public void remove(AutoCloseable object) { + m_closers.remove(object); + } + + /** Closes all objects in the stack. */ + @Override + @SuppressWarnings("PMD.AvoidCatchingGenericException") + public void close() { + for (AutoCloseable autoCloseable : m_closers) { + try { + autoCloseable.close(); + } catch (Exception e) { + // Swallow any exceptions on close + e.printStackTrace(); + } + } + m_closers.clear(); + } +} diff --git a/wpiutil/src/main/java/edu/wpi/first/util/cleanup/ReflectionCleanup.java b/wpiutil/src/main/java/edu/wpi/first/util/cleanup/ReflectionCleanup.java new file mode 100644 index 0000000000..0803028f40 --- /dev/null +++ b/wpiutil/src/main/java/edu/wpi/first/util/cleanup/ReflectionCleanup.java @@ -0,0 +1,50 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package edu.wpi.first.util.cleanup; + +import java.lang.reflect.Field; + +/** + * Implement this interface to have access to a `reflectionCleanup` method that can be called from + * your `close` method, that will use reflection to find all `AutoCloseable` instance members and + * close them. + */ +public interface ReflectionCleanup extends AutoCloseable { + /** + * Default implementation that uses reflection to find all AutoCloseable fields not marked + * SkipCleanup and call close() on them. Call this from your `close()` method with the class level + * you want to close. + * + * @param cls the class level to clean up + */ + @SuppressWarnings("PMD.AvoidCatchingGenericException") + default void reflectionCleanup(Class cls) { + if (!cls.isAssignableFrom(getClass())) { + System.out.println("Passed in class is not assignable from \"this\""); + System.out.println("Expected something in the hierarchy of" + cls.getName()); + System.out.println("This is " + getClass().getName()); + return; + } + for (Field field : cls.getDeclaredFields()) { + if (field.isAnnotationPresent(SkipCleanup.class)) { + continue; + } + if (!AutoCloseable.class.isAssignableFrom(field.getType())) { + continue; + } + if (field.trySetAccessible()) { + try { + AutoCloseable c = (AutoCloseable) field.get(this); + if (c != null) { + c.close(); + } + } catch (Exception e) { + // Ignore any exceptions + e.printStackTrace(); + } + } + } + } +} diff --git a/wpiutil/src/main/java/edu/wpi/first/util/cleanup/SkipCleanup.java b/wpiutil/src/main/java/edu/wpi/first/util/cleanup/SkipCleanup.java new file mode 100644 index 0000000000..e2bc72eaeb --- /dev/null +++ b/wpiutil/src/main/java/edu/wpi/first/util/cleanup/SkipCleanup.java @@ -0,0 +1,14 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package edu.wpi.first.util.cleanup; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface SkipCleanup {} diff --git a/wpiutil/src/test/java/edu/wpi/first/util/cleanup/CleanupPoolTest.java b/wpiutil/src/test/java/edu/wpi/first/util/cleanup/CleanupPoolTest.java new file mode 100644 index 0000000000..a953a855e6 --- /dev/null +++ b/wpiutil/src/test/java/edu/wpi/first/util/cleanup/CleanupPoolTest.java @@ -0,0 +1,154 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package edu.wpi.first.util.cleanup; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; + +class CleanupPoolTest { + static class AutoCloseableObject implements AutoCloseable { + public boolean m_closed; + + @Override + public void close() { + m_closed = true; + } + } + + static class AutoCloseableObjectWithCallback implements AutoCloseable { + private final Runnable m_cb; + + AutoCloseableObjectWithCallback(Runnable cb) { + m_cb = cb; + } + + @Override + public void close() { + m_cb.run(); + } + } + + static class FailingAutoCloseableObject implements AutoCloseable { + public static final String message = "This is an expected failure"; + + @Override + public void close() { + throw new RuntimeException(message); + } + } + + @Test + void cleanupStackWorks() { + List objects = new ArrayList<>(); + objects.add(new AutoCloseableObject()); + objects.add(new AutoCloseableObject()); + objects.add(new AutoCloseableObject()); + + try (CleanupPool pool = new CleanupPool()) { + for (AutoCloseableObject autoCloseableObject : objects) { + pool.register(autoCloseableObject); + } + } + + for (AutoCloseableObject autoCloseableObject : objects) { + assertTrue(autoCloseableObject.m_closed); + } + } + + @Test + @SuppressWarnings("PMD.AvoidCatchingGenericException") + void cleanupStackWithExceptionNotInCloseWorks() { + List objects = new ArrayList<>(); + objects.add(new AutoCloseableObject()); + objects.add(new AutoCloseableObject()); + objects.add(new AutoCloseableObject()); + + String message = "This is a known failure"; + + try (CleanupPool pool = new CleanupPool()) { + for (AutoCloseableObject autoCloseableObject : objects) { + pool.register(autoCloseableObject); + } + throw new Exception(message); + } catch (Exception e) { + assertEquals(message, e.getMessage()); + } + + for (AutoCloseableObject autoCloseableObject : objects) { + assertTrue(autoCloseableObject.m_closed); + } + } + + @Test + @SuppressWarnings("PMD.AvoidCatchingGenericException") + void cleanupStackWithExceptionInCloseWorks() { + List objects = new ArrayList<>(); + objects.add(new AutoCloseableObject()); + objects.add(new AutoCloseableObject()); + objects.add(new AutoCloseableObject()); + + try (CleanupPool pool = new CleanupPool()) { + for (AutoCloseableObject autoCloseableObject : objects) { + pool.register(new FailingAutoCloseableObject()); + pool.register(autoCloseableObject); + } + } + + for (AutoCloseableObject autoCloseableObject : objects) { + assertTrue(autoCloseableObject.m_closed); + } + } + + @Test + void cleanupStackRemovalWorks() { + List objects = new ArrayList<>(); + objects.add(new AutoCloseableObject()); + objects.add(new AutoCloseableObject()); + objects.add(new AutoCloseableObject()); + + try (CleanupPool pool = new CleanupPool()) { + for (AutoCloseableObject autoCloseableObject : objects) { + pool.register(autoCloseableObject); + } + + pool.remove(objects.get(0)); + } + + int idx = 0; + for (AutoCloseableObject autoCloseableObject : objects) { + if (idx == 0) { + assertFalse(autoCloseableObject.m_closed); + } else { + assertTrue(autoCloseableObject.m_closed); + } + idx++; + } + } + + @Test + void cleanupStackIsLifo() { + List objects = new ArrayList<>(); + List order = new ArrayList<>(); + objects.add(new AutoCloseableObjectWithCallback(() -> order.add(0))); + objects.add(new AutoCloseableObjectWithCallback(() -> order.add(1))); + objects.add(new AutoCloseableObjectWithCallback(() -> order.add(2))); + + try (CleanupPool pool = new CleanupPool()) { + for (AutoCloseable autoCloseableObject : objects) { + pool.register(autoCloseableObject); + } + } + + assertEquals(order.size(), 3); + assertEquals(order.get(0), 2); + assertEquals(order.get(1), 1); + assertEquals(order.get(2), 0); + } +} diff --git a/wpiutil/src/test/java/edu/wpi/first/util/cleanup/ReflectionCleanupTest.java b/wpiutil/src/test/java/edu/wpi/first/util/cleanup/ReflectionCleanupTest.java new file mode 100644 index 0000000000..52fafb4414 --- /dev/null +++ b/wpiutil/src/test/java/edu/wpi/first/util/cleanup/ReflectionCleanupTest.java @@ -0,0 +1,61 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +package edu.wpi.first.util.cleanup; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class ReflectionCleanupTest { + static class CleanupClass implements AutoCloseable { + public boolean m_closed; + + @Override + public void close() { + m_closed = true; + } + } + + static class CleanupTest implements ReflectionCleanup { + public CleanupClass m_class1 = new CleanupClass(); + public CleanupClass m_class2 = new CleanupClass(); + public Object m_nonCleanupObject = new Object(); + public Object m_nullCleanupObject; + + @Override + public void close() { + reflectionCleanup(CleanupTest.class); + } + } + + static class CleanupTest2 extends CleanupTest { + @SkipCleanup public CleanupClass m_class3 = new CleanupClass(); + public CleanupClass m_class4 = new CleanupClass(); + + @Override + public void close() { + reflectionCleanup(CleanupTest2.class); + } + } + + @Test + void cleanupClosesAllFields() { + CleanupTest test = new CleanupTest(); + test.close(); + assertTrue(test.m_class1.m_closed); + assertTrue(test.m_class2.m_closed); + } + + @Test + void cleanupOnlyClosesExplicitClassAndSkipWorks() { + CleanupTest2 test = new CleanupTest2(); + test.close(); + assertFalse(test.m_class1.m_closed); + assertFalse(test.m_class2.m_closed); + assertFalse(test.m_class3.m_closed); + assertTrue(test.m_class4.m_closed); + } +}