Skip to content

Commit

Permalink
Moved reflection code from a05annexRobot to here, augmented it with i…
Browse files Browse the repository at this point in the history
…nstantiation arguments to allow more options in the SwervePathPlanning. Added tests, incremented this library version to 2024.0.1 th change the versioning scheme to better match the WPI library versioning.
  • Loading branch information
STHobbes committed Jan 2, 2025
1 parent 3ad89a0 commit 4068700
Show file tree
Hide file tree
Showing 8 changed files with 275 additions and 9 deletions.
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
* **version:** 0.9.6
* **version:** 2025.0.1
* **status:** released (first release version: 0.8.5)
* **comments:** We have been using this library for robot development since December 2020, and
believe it is ready for general use.
Expand Down Expand Up @@ -35,7 +35,8 @@ used regardless of the FTC/FRC base platform libraries and/or hardware.
* **`AngleTYpe`** - The angle unit specification..
* **`JsonSupport`** - a class with helper functions to aid in reading/writing JSON files.
* **`Utl`** - a class which extends the java `Math` class with variable argument `min()`, `max()`,
`length()`, and `clip()` functions.
`length()`, and `clip()` functions, also includes methods using reflection for instantiation
and field modification.

## Including a05annexUtil in your build.gradle

Expand All @@ -51,7 +52,7 @@ There are a couple paths for inclusion.
Simply add it to the dependencies section of your `gradle.build` file as:
```
dependencies {
implementation 'org.a05annex:a05annexUtil:0.9.6'
implementation 'org.a05annex:a05annexUtil:2025.0.1'
.
.
.
Expand All @@ -66,16 +67,21 @@ The next most simple way to use **a05annexUtil**, following the advice from this
[chiefdelphi post](https://www.chiefdelphi.com/t/adding-my-teams-library-as-a-vendor-library/339626)
and advises you:
* create a `libs` folder in your robot project
* copy the `a05annxUtil-0.9.6.jar` file from the github 0.9.6 release into that `libs` folder
* copy the `a05annxUtil-2025.0.1.jar` file from the github 0.9.6 release into that `libs` folder
* in the dependencies section of the `build.gradle` file add the line:
`implementation fileTree(dir: 'libs', include: ['*.jar'])`
* add the `libs/a05annxUtil-0.9.6.jar` to **git** so it is saved as part of your project.
* add the `libs/a05annxUtil-2025.0.1.jar` to **git** so it is saved as part of your project.

The disadvantage of this method is that you must manually download the library and
put it in your project, you also need to check for version updates.

## Release Notes

* version 2025.0.1 - ?-Jan 2025
* changed the versioning to be consistent with WPI versioning.
* Added Util.setOnce(...) to allow setting library constants from the implementation
* Added Util.instantiateObjectFromName(...) for better support of season extension with
tools like the swerve path planner.
* version 0.9.6 - 05-Dec-2023 - 2023 Charged Up season and post season improvements:
* Added `Utl.inTolerance(...)` method for testing whether a value is within a specified tolerance
of a target value.
Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ dependencies {

sourceCompatibility = '1.9'
targetCompatibility = '1.9'
version = '0.9.6'
version = '2025.0.1'
group = 'org.a05annex'

java {
Expand Down
75 changes: 72 additions & 3 deletions src/main/java/org/a05annex/util/Utl.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.a05annex.util;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
Expand Down Expand Up @@ -110,9 +111,9 @@ public static boolean inTolerance(double value, double target, double tolerance)
* @param newValue The value to set in the {@code instance} and {@code fieldName}. It must match the type of
* {@code fieldName}.
* @return The value that was successfully set in the instance.
* @throws IllegalStateException If a suitable field in the instance is already set,
* if no suitable field exists, or if there is an error
* accessing instance fields.
* @throws IllegalArgumentException Thrown if a field named {@code fieldName} was not found, or if the
* {@code newValue} type could not be used as the value for {@code fieldName}.
* @throws IllegalStateException Thrown if {@code fieldName} has already been set.
*/
public static <T> T setOnce(@NotNull Object instance, String fieldName, @NotNull T newValue) {
try {
Expand Down Expand Up @@ -189,4 +190,72 @@ public static <T> T setOnce(T newValue, Class<?> clazz) {

throw new IllegalStateException("No suitable static variable was found to set.");
}

// -----------------------------------------------------------------------------------------------------------------
// Instantiation by reflection - we are finding increasingly more cases where we are writing library code that
// implements extensibility through reflection. For example, our path planning software lets the user specify
// commands to be run by command class name and arguments - but it has no access to the actual robot code it only
// knows the command class and arguments. In the robot library, the command that runs the path knows about the wpi
// library code structure - but doesn't know what has been programmed for this year's robot, so it needs to make
// sure and instantiated object is (perhaps) an instance of a library type, or, implements some library interfaces.
//
// The next couple methods implement reflection instantiation of both a no-argument constructor, and a constructor
// with arguments, with checks that the constructed object is of the expected type and implements the
// expected interfaces.
// -----------------------------------------------------------------------------------------------------------------
/**
* Instantiate an object using a constructor with a specified argument signature and initial values.
* @param <T> The type object we are expecting to instantiate. Note, this may be a base class extended by
* {@code clazzName} or an interface implemented by {@code clazzName}
* @param returnClazz The class of type {@code <T>}.
* @param clazzName The fully qualified object class name.
* @param parameterTypes An array of the object classes for constructor arguments.
* @param instArgs An array of the values for constructor arguments.
* @return Returns the instantiated object, or {@code null} if the object could not be instantiated.
*/
public static <T> T instantiateObjectFromName(@NotNull Class<T> returnClazz, @NotNull String clazzName,
@NotNull Class<?>[] parameterTypes, @NotNull Object[] instArgs) {
try {
Object obj = null;
Class clazz = Class.forName(clazzName);
if (parameterTypes.length != instArgs.length) {
System.out.printf("Could not instantiate object: class='%s':\n", clazzName);
System.out.printf(" 'parameterTypes' list length (%d) is not equal to 'instArgs' length (%d)\n",
parameterTypes.length, instArgs.length);
return null;
} else if (0 == parameterTypes.length) {
obj = clazz.getDeclaredConstructor().newInstance();
} else {
obj = clazz.getDeclaredConstructor(parameterTypes).newInstance(instArgs);
}
return returnClazz.cast(obj);
} catch (final ClassCastException t) {
System.out.printf("Could not instantiate object: class='%s' as a '%s'\n",
clazzName, returnClazz.getCanonicalName());
} catch (final ClassNotFoundException t) {
System.out.printf("Could not instantiate object: class='%s'; class not found.\n", clazzName);
} catch (final NoSuchMethodException t) {
System.out.printf(
"Could not instantiate object: class='%s'; no constructor matching the 'parameterTypes'.\n",
clazzName);
} catch (final IllegalArgumentException t) {
System.out.printf(
"Could not instantiate object: class='%s'; the 'instArgs' types do not match the 'parameterTypes'.\n",
clazzName);
} catch (final Exception t) {
System.out.printf("Could not instantiate object: class='%s' - no details.\n", clazzName);
}
return null;
}
/**
* Instantiate an object using the default no argument constructor
* @param <T> The type object we are expecting to instantiate. Note, this may be a base class extended by
* {@code clazzName} or an interface implemented by {@code clazzName}
* @param returnClazz The class of type {@code <T>}.
* @param clazzName The fully qualified object class name.
* @return Returns the instantiated object, or {@code null} if the object could not be instantiated.
*/
public static <T> T instantiateObjectFromName(@NotNull Class<T> returnClazz, @NotNull String clazzName) {
return instantiateObjectFromName(returnClazz, clazzName, new Class<?>[] {}, new Object[] {});
}
}
140 changes: 140 additions & 0 deletions src/test/java/org/a05annex/util/TestInstantiate.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package org.a05annex.util;

import org.a05annex.util.instantiate.*;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.platform.runner.JUnitPlatform;
import org.junit.runner.RunWith;

import static org.junit.jupiter.api.Assertions.*;


@RunWith(JUnitPlatform.class)
public class TestInstantiate {

final String TEST_OBJ_CLASS_NAME = TestObj.class.getCanonicalName();
final String TEST_OBJ_BASE_CLASS_NAME = TestObjBase.class.getCanonicalName();
final String TEST_OBJ_EXTENDS_CLASS_NAME = TestObjExtendsBase.class.getCanonicalName();
final String TEST_OBJ_INTERFACE_CLASS_NAME = ITestInterface.class.getCanonicalName();

// *****************************************************************************************************************
// The basic tests for a no argument constructor to let us get the details right for:
// * class names;
// * casting the instantiated object to a base class or interface;
// *****************************************************************************************************************
/**
* This is a test for basic instantiation with a no-argument constructor. This is an unlikely real
* scenario because this type of reflection is generally for classes that are extending functionality
* by extending a base class or implementing an interface and whose class name is unknown to the
* who will be using the class, and is being dynamically loaded based on a data file referring to the
* class.
*/
@Test
void testBasicInstantiation() {
TestObj obj = Utl.instantiateObjectFromName(TestObj.class, TEST_OBJ_CLASS_NAME);
assertNotNull(obj);
assertTrue(obj instanceof TestObj);
}

/**
* Testing a typical usage where the object being instantiated by reflection extends a base object
* implementation.
*/
@Test
void testBasicInstantiationExtends() {
TestObjBase obj = Utl.instantiateObjectFromName(TestObjBase.class, TEST_OBJ_EXTENDS_CLASS_NAME);
assertNotNull(obj);
assertTrue(obj instanceof TestObjBase);
assertTrue(obj instanceof TestObjExtendsBase);
assertNull(obj.getBoolField());
assertNull(obj.getIntField());
assertNull(obj.getStrField());
}
@Test
/**
* Testing a typical usage where the object being instantiated by reflection implements an interface
*/
void testBasicInstantiationImplements() {
ITestInterface obj = Utl.instantiateObjectFromName(ITestInterface.class, TEST_OBJ_EXTENDS_CLASS_NAME);
assertNotNull(obj);
assertTrue(obj instanceof ITestInterface);
assertTrue(obj instanceof TestObjExtendsBase);
assertTrue(obj.getTestInterfaceValue());
}

/**
* A test where the instantiated class instance cannot be cast to the expected class.
*/
@Test
void testBasicInstantiationClassMismatch() {
TestObjBase obj = Utl.instantiateObjectFromName(TestObjBase.class, TEST_OBJ_CLASS_NAME);
assertNull(obj);
}

/**
* A test where the requested class does not exist - an invalid class name is specified.
*/
@Test
void testBasicInstantiationBadClassName() {
TestObjBase obj = Utl.instantiateObjectFromName(TestObjBase.class, "StupidJunkClassName");
assertNull(obj);
}

@Test
void testInstantiationExtendsWithArgs() {
Boolean boolValue = true;
Integer intValue = 12;
String strValue = "This is a test String";
TestObjBase obj = Utl.instantiateObjectFromName(TestObjBase.class, TEST_OBJ_EXTENDS_CLASS_NAME,
new Class<?>[] {Boolean.class, Integer.class, String.class},
new Object[] {boolValue, intValue, strValue});
assertNotNull(obj);
assertTrue(obj instanceof TestObjBase);
assertTrue(obj instanceof TestObjExtendsBase);
assertEquals(boolValue, obj.getBoolField());
assertEquals(intValue, obj.getIntField());
assertEquals(strValue, obj.getStrField());
}

/**
* Test with mismatched {@code parameterTypes} and {@code instArgs} array lengths.
*/
@Test
void testInstantiationExtendsWithBadArgs1() {
Boolean boolValue = true;
Integer intValue = 12;
String strValue = "This is a test String";
TestObjBase obj = Utl.instantiateObjectFromName(TestObjBase.class, TEST_OBJ_EXTENDS_CLASS_NAME,
new Class<?>[] {Boolean.class, Integer.class, String.class},
new Object[] {boolValue, intValue});
assertNull(obj);
}

/**
* Test with an argument type order that does not match the constructor.
*/
@Test
void testInstantiationExtendsWithBadArgs2() {
Boolean boolValue = true;
Integer intValue = 12;
String strValue = "This is a test String";
TestObjBase obj = Utl.instantiateObjectFromName(TestObjBase.class, TEST_OBJ_EXTENDS_CLASS_NAME,
new Class<?>[] {Boolean.class, String.class, Integer.class},
new Object[] {boolValue, strValue, intValue});
assertNull(obj);
}
/**
* Test with an the {@code instArgs} types do not match the {@code parameterTypes}.
*/
@Test
void testInstantiationExtendsWithBadArgs3() {
Boolean boolValue = true;
Integer intValue = 12;
String strValue = "This is a test String";
TestObjBase obj = Utl.instantiateObjectFromName(TestObjBase.class, TEST_OBJ_EXTENDS_CLASS_NAME,
new Class<?>[] {Boolean.class, Integer.class, String.class},
new Object[] {boolValue, strValue, intValue});
assertNull(obj);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package org.a05annex.util.instantiate;

public interface ITestInterface {
boolean getTestInterfaceValue();
}
5 changes: 5 additions & 0 deletions src/test/java/org/a05annex/util/instantiate/TestObj.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package org.a05annex.util.instantiate;

public class TestObj{
public TestObj() {}
}
26 changes: 26 additions & 0 deletions src/test/java/org/a05annex/util/instantiate/TestObjBase.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.a05annex.util.instantiate;

public class TestObjBase {
private final Boolean boolField ;
private final Integer intField;
private final String strField;
public TestObjBase() {
this.boolField = null;
this.intField = null;
this.strField = null;
}
TestObjBase(Boolean boolField, Integer intField, String strField) {
this.boolField = boolField;
this.intField = intField;
this.strField = strField;
}
public Boolean getBoolField() {
return boolField;
}
public Integer getIntField() {
return intField;
}
public String getStrField() {
return strField;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.a05annex.util.instantiate;

public class TestObjExtendsBase extends TestObjBase implements ITestInterface{
public TestObjExtendsBase() {
super();
}
public TestObjExtendsBase(Boolean boolField, Integer intField, String strField) {
super(boolField, intField, strField);
}
@Override
public boolean getTestInterfaceValue() {
return true;
}
}

0 comments on commit 4068700

Please sign in to comment.