Skip to content

Commit

Permalink
#97 Make ArrayComponentType public, add ClassUtils#isInstance
Browse files Browse the repository at this point in the history
  • Loading branch information
ljacqu committed Aug 24, 2023
1 parent 0ed5207 commit 26c1df8
Show file tree
Hide file tree
Showing 6 changed files with 196 additions and 70 deletions.
81 changes: 81 additions & 0 deletions src/main/java/ch/jalu/typeresolver/array/ArrayComponentType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package ch.jalu.typeresolver.array;

import ch.jalu.typeresolver.classutil.ClassUtils;
import org.jetbrains.annotations.Nullable;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

/**
* Represents the component <i>type</i> an array can have: this enum has all primitive types, and Object.
*/
public enum ArrayComponentType {

BOOLEAN(boolean.class),
BYTE(byte.class),
CHARACTER(char.class),
SHORT(short.class),
INTEGER(int.class),
LONG(long.class),
FLOAT(float.class),
DOUBLE(double.class),
/** Means any extension of Object, such as the component of {@code String[]}, or even {@code int[][]}. */
OBJECT(Object.class);

private static final Map<Class<?>, ArrayComponentType> PRIMITIVE_ARRAY_TO_COMPONENT =
createPrimitiveArrayTypesToComponentMap();
private final Class<?> componentClass;

/**
* Constructor.
*
* @param componentClass the base component type (primitive class, or Object.class)
*/
ArrayComponentType(Class<?> componentClass) {
this.componentClass = componentClass;
}

/**
* @return The class that is the component type of the array. This is a primitive class like {@code int.class},
* or {@code Object.class} if the array's component type is a reference type.
*/
public Class<?> getComponentClass() {
return componentClass;
}

/**
* Returns the appropriate {@link ArrayComponentType} entry for the given array. Throws an exception if
* the argument is not an array.
*
* @param array the array to inspect
* @return the array component type representing the enum's component type
*/
public static ArrayComponentType getArrayComponentType(@Nullable Object array) {
if (array != null) {
Class<?> clazz = array.getClass();
ArrayComponentType componentType = PRIMITIVE_ARRAY_TO_COMPONENT.get(clazz);
if (componentType != null) {
return componentType;
} else if (Object[].class.isAssignableFrom(clazz)) {
return ArrayComponentType.OBJECT;
}
}

throw new IllegalArgumentException("Expected an array but got an object of type: "
+ ClassUtils.getClassName(array));
}

private static Map<Class<?>, ArrayComponentType> createPrimitiveArrayTypesToComponentMap() {
Map<Class<?>, ArrayComponentType> map = new HashMap<>();
map.put(boolean[].class, BOOLEAN);
map.put(byte[].class, BYTE);
map.put(char[].class, CHARACTER);
map.put(short[].class, SHORT);
map.put(int[].class, INTEGER);
map.put(long[].class, LONG);
map.put(float[].class, FLOAT);
map.put(double[].class, DOUBLE);
return Collections.unmodifiableMap(map);
}
}
71 changes: 5 additions & 66 deletions src/main/java/ch/jalu/typeresolver/array/ArrayUtils.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
package ch.jalu.typeresolver.array;

import org.jetbrains.annotations.Nullable;
import ch.jalu.typeresolver.classutil.ClassUtils;

import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import static ch.jalu.typeresolver.array.ArrayComponentType.getArrayComponentType;

/**
* This class contains methods that can handle arrays of any type ({@code boolean[]}, {@code byte[]}, {@code Object[]},
* etc.). Most methods delegate to methods in {@link Arrays} with the appropriate signature. The methods in this class
Expand Down Expand Up @@ -398,74 +397,14 @@ public static void simpleBooleanArraySort(boolean[] array, int fromIndex, int to
Arrays.fill(array, fromIndex + numberOfFalse, toIndex, true);
}

private static ArrayComponentType getArrayComponentType(Object array) {
if (array == null) {
throw new NullPointerException("array");
}
ArrayComponentType componentType = ArrayComponentType.resolveComponentForPrimitiveArrays(array.getClass());
if (componentType != null) {
return componentType;
}
if (Object[].class.isAssignableFrom(array.getClass())) {
return ArrayComponentType.OBJECT;
}
throw new IllegalArgumentException("Expected an array as argument, but got: " + array.getClass());
}

private static void verifyArgumentMatchesComponentType(Object argument, ArrayComponentType componentType,
String argumentName) {
if (argument == null) {
throw new NullPointerException(argumentName);
} else if (!componentType.matches(argument)) {
}
if (!ClassUtils.isInstance(argument, componentType.getComponentClass())) {
throw new ClassCastException("Expected " + argumentName + " to be a "
+ componentType.name().toLowerCase(Locale.ROOT) + ", instead found: " + argument.getClass());
}
}

/**
* Represents the component type an array can have.
*/
enum ArrayComponentType {

BOOLEAN(Boolean.class),
BYTE(Byte.class),
CHARACTER(Character.class),
SHORT(Short.class),
INTEGER(Integer.class),
LONG(Long.class),
FLOAT(Float.class),
DOUBLE(Double.class),
/** Means any extension of Object, such as the component of {@code String[]}, or even {@code int[][]}. */
OBJECT(Object.class);

private static final Map<Class<?>, ArrayComponentType> PRIMITIVE_ARRAY_TO_COMPONENT =
createPrimitiveArrayTypesToComponentMap();
private final Class<?> referenceType;

ArrayComponentType(Class<?> referenceType) {
this.referenceType = referenceType;
}

boolean matches(Object obj) {
return referenceType.isInstance(obj);
}

@Nullable
static ArrayComponentType resolveComponentForPrimitiveArrays(Class<?> clazz) {
return PRIMITIVE_ARRAY_TO_COMPONENT.get(clazz);
}

private static Map<Class<?>, ArrayComponentType> createPrimitiveArrayTypesToComponentMap() {
Map<Class<?>, ArrayComponentType> map = new HashMap<>();
map.put(boolean[].class, BOOLEAN);
map.put(byte[].class, BYTE);
map.put(char[].class, CHARACTER);
map.put(short[].class, SHORT);
map.put(int[].class, INTEGER);
map.put(long[].class, LONG);
map.put(float[].class, FLOAT);
map.put(double[].class, DOUBLE);
return Collections.unmodifiableMap(map);
}
}
}
13 changes: 13 additions & 0 deletions src/main/java/ch/jalu/typeresolver/classutil/ClassUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,19 @@ public static Class<?> loadClassOrThrow(String name) {
}
}

/**
* Returns whether the given object is an instance of the given target type. This method, unlike
* {@link Class#isInstance}, boxes primitive classes, such that {@code isInstance(5, int.class)}
* returns true. See also {@link ClassUtils#tryCast} if you want to cast the object safely.
*
* @param object the object to process
* @param targetType the target type to check for
* @return true if the object is not null and an instance of the given target type, false otherwise
*/
public static boolean isInstance(@Nullable Object object, Class<?> targetType) {
return PrimitiveType.toReferenceType(targetType).isInstance(object);
}

/**
* Returns an optional with the given object cast to the target type, or an empty optional if the given object
* cannot be cast to the specified type. See {@link #tryCast(Object, Class, boolean)} for details.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package ch.jalu.typeresolver.array;

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 java.util.stream.Stream;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.jupiter.api.Assertions.assertThrows;

/**
* Test for {@link ArrayComponentType}.
*/
class ArrayComponentTypeTest {

@ParameterizedTest
@MethodSource("argsForDetermineArrayTypeTest")
void shouldDetermineArrayType(Object array, ArrayComponentType expectedType) {
// given / when
ArrayComponentType result = ArrayComponentType.getArrayComponentType(array);

// then
assertThat(result, equalTo(expectedType));
if (result == ArrayComponentType.OBJECT) {
assertThat(result.getComponentClass(), equalTo(Object.class));
} else {
assertThat(result.getComponentClass(), equalTo(array.getClass().getComponentType()));
}
}

static Stream<Arguments> argsForDetermineArrayTypeTest() {
return Stream.of(
Arguments.of(new boolean[]{true}, ArrayComponentType.BOOLEAN),
Arguments.of(new byte[]{}, ArrayComponentType.BYTE),
Arguments.of(new char[]{'ç'}, ArrayComponentType.CHARACTER),
Arguments.of(new short[]{}, ArrayComponentType.SHORT),
Arguments.of(new int[]{22}, ArrayComponentType.INTEGER),
Arguments.of(new long[]{420}, ArrayComponentType.LONG),
Arguments.of(new float[]{}, ArrayComponentType.FLOAT),
Arguments.of(new double[]{3.4}, ArrayComponentType.DOUBLE),

Arguments.of(new Object[]{'5'}, ArrayComponentType.OBJECT),
Arguments.of(new Boolean[]{true}, ArrayComponentType.OBJECT),
Arguments.of(new Double[]{6.2}, ArrayComponentType.OBJECT),
Arguments.of(new String[]{"t"}, ArrayComponentType.OBJECT),
Arguments.of(new String[][]{}, ArrayComponentType.OBJECT),
Arguments.of(new double[][]{{54.2}}, ArrayComponentType.OBJECT),
Arguments.of(new char[][][]{{{'ê'}}}, ArrayComponentType.OBJECT)
);
}

@Test
void shouldThrowForNull() {
// given / when
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
() -> ArrayComponentType.getArrayComponentType(null));

// then
assertThat(ex.getMessage(), equalTo("Expected an array but got an object of type: null"));
}

@Test
void shouldThrowForNonArrayValue() {
// given / when
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
() -> ArrayComponentType.getArrayComponentType("test"));

// then
assertThat(ex.getMessage(), equalTo("Expected an array but got an object of type: java.lang.String"));
}
}
7 changes: 3 additions & 4 deletions src/test/java/ch/jalu/typeresolver/array/ArrayUtilsTest.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package ch.jalu.typeresolver.array;

import ch.jalu.typeresolver.EnumUtils;
import ch.jalu.typeresolver.array.ArrayUtils.ArrayComponentType;
import ch.jalu.typeresolver.primitives.PrimitiveType;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -350,7 +349,7 @@ void shouldHaveMessageAboutTypeMismatch() {
// given / when
IllegalArgumentException iae1 = assertThrows(IllegalArgumentException.class,
() -> ArrayUtils.copyOf(BigDecimal.TEN, 2));
NullPointerException npe2 = assertThrows(NullPointerException.class,
IllegalArgumentException iae2 = assertThrows(IllegalArgumentException.class,
() -> ArrayUtils.hashCode(null));
IllegalArgumentException iae3 = assertThrows(IllegalArgumentException.class,
() -> ArrayUtils.fill(new Object(), "t"));
Expand All @@ -360,8 +359,8 @@ void shouldHaveMessageAboutTypeMismatch() {
() -> ArrayUtils.fill(new double[]{2.0, 1.85}, null));

// then
assertThat(iae1.getMessage(), equalTo("Expected an array as argument, but got: class java.math.BigDecimal"));
assertThat(npe2.getMessage(), equalTo("array"));
assertThat(iae1.getMessage(), equalTo("Expected an array but got an object of type: java.math.BigDecimal"));
assertThat(iae2.getMessage(), equalTo("Expected an array but got an object of type: null"));
assertThat(iae3.getMessage(), equalTo("Argument is not an array"));
assertThat(cce4.getMessage(), equalTo("Expected val to be a integer, instead found: class java.lang.String"));
assertThat(npe5.getMessage(), equalTo("val"));
Expand Down
20 changes: 20 additions & 0 deletions src/test/java/ch/jalu/typeresolver/classutil/ClassUtilsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,26 @@ void shouldLoadClassOrThrowException() {
@Nested
class SubClassAndCastingUtils {

@Test
void shouldDetermineWhetherObjectIsInstanceOfClassInclAutoboxing() {
// given / when / then
assertTrue(ClassUtils.isInstance("FGH", String.class));
assertTrue(ClassUtils.isInstance("FGH", CharSequence.class));
assertTrue(ClassUtils.isInstance("FGH", Serializable.class));
assertTrue(ClassUtils.isInstance(3, Integer.class));
assertTrue(ClassUtils.isInstance(3, Number.class));
assertTrue(ClassUtils.isInstance(3, int.class));
assertTrue(ClassUtils.isInstance(new byte[0], byte[].class));

assertFalse(ClassUtils.isInstance(null, String.class));
assertFalse(ClassUtils.isInstance(null, char.class));
assertFalse(ClassUtils.isInstance(4, String.class));
assertFalse(ClassUtils.isInstance(4, double.class));
assertFalse(ClassUtils.isInstance(4, Double.class));
assertFalse(ClassUtils.isInstance(new int[0], Integer[].class));
assertFalse(ClassUtils.isInstance(new Integer[0], int[].class));
}

// This test just has a few samples. The method below tests this method more intensively; the goal here
// is to just ensure that the return value is typed correctly, and that the 2-params method has autobox=true
@Test
Expand Down

0 comments on commit 26c1df8

Please sign in to comment.