diff --git a/src/main/java/ca/trackerforce/DotUtils.java b/src/main/java/ca/trackerforce/DotUtils.java index c32d0f4..d949a2d 100644 --- a/src/main/java/ca/trackerforce/DotUtils.java +++ b/src/main/java/ca/trackerforce/DotUtils.java @@ -2,6 +2,7 @@ import ca.trackerforce.path.DotPathFactory; +import java.lang.reflect.Array; import java.util.Collections; import java.util.List; import java.util.Map; @@ -33,11 +34,13 @@ public static List parsePaths(String path) { * @throws ClassCastException if the property is not a map */ public static Map mapFrom(Map source, String property) { - if (isInvalid(source, property)) { + Object result = getObjectFromSource(source, property); + + if (!(result instanceof Map)) { return Collections.emptyMap(); } - return (Map) source.get(property); + return (Map) result; } /** @@ -49,31 +52,84 @@ public static Map mapFrom(Map source, String pro * @throws ClassCastException if the property is not a list of maps */ public static List> listFrom(Map source, String property) { - if (isInvalid(source, property)) { + Object result = getObjectFromSource(source, property); + + if (!(result instanceof List)) { return Collections.emptyList(); } - return (List>) source.get(property); + return (List>) result; + } + + /** + * Extracts a typed list from the source map based on the specified property. + * + * @param source the source map + * @param property the property to extract or a dot-notated path for nested properties + * @return the extracted list of maps or an empty list if not found + * @throws ClassCastException if the property is not a list of maps + */ + public static List listFrom(Map source, String property, Class clazz) { + Object result = getObjectFromSource(source, property); + + if (!(result instanceof List) || ((List) result).isEmpty() || !clazz.isInstance(((List) result).get(0))) { + return Collections.emptyList(); + } + + return (List) result; } /** * Extracts a list of objects from the source map based on the specified property. * * @param source the source map - * @param property the property to extract + * @param property the property to extract or a dot-notated path for nested properties * @return the extracted list of objects or an empty list if not found * @throws ClassCastException if the property is not a list of objects */ public static Object[] arrayFrom(Map source, String property) { - if (isInvalid(source, property)) { + Object result = getObjectFromSource(source, property); + + if (result == null || !result.getClass().isArray()) { return new Object[0]; } - return (Object[]) source.get(property); + return convertToObjectArray(result); } - private static boolean isInvalid(Map source, String property) { - return source == null || property == null || property.isEmpty() || !source.containsKey(property); + private static Object getObjectFromSource(Map source, String property) { + if (property.contains(".")) { + String[] keys = property.split("\\."); + + for (int i = 0; i < keys.length - 1; i++) { + source = (Map) source.get(keys[i]); + } + + return source.get(keys[keys.length - 1]); + } + + if (source == null || !source.containsKey(property)) { + return null; + } + + return source.get(property); + } + + private static Object[] convertToObjectArray(Object array) { + Class componentType = array.getClass().getComponentType(); + + if (!componentType.isPrimitive()) { + return (Object[]) array; + } + + int length = Array.getLength(array); + Object[] result = new Object[length]; + + for (int i = 0; i < length; i++) { + result[i] = Array.get(array, i); + } + + return result; } } diff --git a/src/main/java/ca/trackerforce/path/PathCommon.java b/src/main/java/ca/trackerforce/path/PathCommon.java index 4107195..2753159 100644 --- a/src/main/java/ca/trackerforce/path/PathCommon.java +++ b/src/main/java/ca/trackerforce/path/PathCommon.java @@ -100,6 +100,11 @@ protected Object getPropertyValue(T source, String propertyName) { return getterResult; } + // Try mapping method for Map instances + if (source instanceof Map map && map.containsKey(propertyName)) { + return map.get(propertyName); + } + // Fall back to direct field access return tryDirectFieldAccess(source, propertyName, clazz); } catch (Exception e) { diff --git a/src/test/java/ca/trackerforce/DotUtilsTest.java b/src/test/java/ca/trackerforce/DotUtilsTest.java index cf37ff8..c9e3916 100644 --- a/src/test/java/ca/trackerforce/DotUtilsTest.java +++ b/src/test/java/ca/trackerforce/DotUtilsTest.java @@ -1,6 +1,5 @@ package ca.trackerforce; -import ca.trackerforce.fixture.record.UserDetail; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -17,7 +16,7 @@ class DotUtilsTest { static Stream userDetailProvider() { return Stream.of( - Arguments.of("Record type", UserDetail.of()), + Arguments.of("Record type", ca.trackerforce.fixture.record.UserDetail.of()), Arguments.of("Class type", ca.trackerforce.fixture.clazz.UserDetail.of()) ); } @@ -27,13 +26,19 @@ static Stream userDetailProvider() { void shouldReturnSelectedArrayProperties(String implementation, Object userDetail) { // When var result = dotPathQL.toMap(userDetail); - var roles = DotUtils.arrayFrom(result, "roles"); + var roles = DotUtils.arrayFrom(result, "roles"); // simple array + var locationsHomeCoordinates = DotUtils.arrayFrom(result, "locations.home.coordinates"); // nested array // Then assertNotNull(roles); assertEquals(2, roles.length); assertEquals("USER", roles[0]); assertEquals("ADMIN", roles[1]); + + assertNotNull(locationsHomeCoordinates); + assertEquals(2, locationsHomeCoordinates.length); + assertEquals(39, locationsHomeCoordinates[0]); + assertEquals(89, locationsHomeCoordinates[1]); } @ParameterizedTest(name = "{0}") @@ -42,10 +47,16 @@ void shouldReturnSelectedListProperties(String implementation, Object userDetail // When var result = dotPathQL.toMap(userDetail); var occupations = DotUtils.listFrom(result, "occupations"); + var locationsWorkNumbers = DotUtils.listFrom(result, "locations.work.numbers", Integer.class); // Then assertNotNull(occupations); assertEquals(2, occupations.size()); + + assertNotNull(locationsWorkNumbers); + assertEquals(5, locationsWorkNumbers.size()); + assertEquals(11, locationsWorkNumbers.get(0)); + assertEquals(15, locationsWorkNumbers.get(4)); } @ParameterizedTest(name = "{0}") diff --git a/src/test/java/ca/trackerforce/ExcludeTypeClassRecordTest.java b/src/test/java/ca/trackerforce/ExcludeTypeClassRecordTest.java index 5cd5cec..8a675ad 100644 --- a/src/test/java/ca/trackerforce/ExcludeTypeClassRecordTest.java +++ b/src/test/java/ca/trackerforce/ExcludeTypeClassRecordTest.java @@ -1,6 +1,5 @@ package ca.trackerforce; -import ca.trackerforce.fixture.record.UserDetail; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -17,7 +16,7 @@ class ExcludeTypeClassRecordTest { static Stream userDetailProvider() { return Stream.of( - Arguments.of("Record type", UserDetail.of()), + Arguments.of("Record type", ca.trackerforce.fixture.record.UserDetail.of()), Arguments.of("Class type", ca.trackerforce.fixture.clazz.UserDetail.of()) ); } diff --git a/src/test/java/ca/trackerforce/FilterTypeClassRecordTest.java b/src/test/java/ca/trackerforce/FilterTypeClassRecordTest.java index 3ea8486..38509da 100644 --- a/src/test/java/ca/trackerforce/FilterTypeClassRecordTest.java +++ b/src/test/java/ca/trackerforce/FilterTypeClassRecordTest.java @@ -1,6 +1,5 @@ package ca.trackerforce; -import ca.trackerforce.fixture.record.UserDetail; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -17,7 +16,7 @@ class FilterTypeClassRecordTest { static Stream userDetailProvider() { return Stream.of( - Arguments.of("Record type", UserDetail.of()), + Arguments.of("Record type", ca.trackerforce.fixture.record.UserDetail.of()), Arguments.of("Class type", ca.trackerforce.fixture.clazz.UserDetail.of()) ); } @@ -185,6 +184,24 @@ void shouldReturnFilteredObjectUsingNestedGroupedPaths(String implementation, Ob assertEquals("Springfield", workLocation.get("city")); } + @ParameterizedTest(name = "{0}") + @MethodSource("userDetailProvider") + void shouldReturnFilteredMapAttributes(String implementation, Object userDetail) { + // When + var source = dotPathQL.toMap(userDetail); // Convert to Map for testing + var result = dotPathQL.filter(source, List.of( + "username", + "address.street", + "orders.products.name" + )); + + // Then + assertEquals(3, result.size()); + assertEquals("john_doe", result.get("username")); + assertEquals(1, DotUtils.mapFrom(result, "address").size()); + assertEquals(2, DotUtils.listFrom(result, "orders").size()); + } + @ParameterizedTest(name = "{0}") @MethodSource("userDetailProvider") void shouldReturnEmptyResultInvalidGroupedPaths(String implementation, Object userDetail) { @@ -204,4 +221,5 @@ void shouldReturnEmptyMapWhenSourceIsNull() { assertNotNull(result); assertTrue(result.isEmpty()); } + } diff --git a/src/test/java/ca/trackerforce/PrintTypeClassRecordTest.java b/src/test/java/ca/trackerforce/PrintTypeClassRecordTest.java index 6194ca7..06c724f 100644 --- a/src/test/java/ca/trackerforce/PrintTypeClassRecordTest.java +++ b/src/test/java/ca/trackerforce/PrintTypeClassRecordTest.java @@ -1,6 +1,5 @@ package ca.trackerforce; -import ca.trackerforce.fixture.record.UserDetail; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -18,7 +17,7 @@ class PrintTypeClassRecordTest { static Stream userDetailProvider() { return Stream.of( - Arguments.of("Record type", UserDetail.of()), + Arguments.of("Record type", ca.trackerforce.fixture.record.UserDetail.of()), Arguments.of("Class type", ca.trackerforce.fixture.clazz.UserDetail.of()) ); }