Skip to content

Commit e8a1fdb

Browse files
authored
Improved DotUtils for better type handling (#9)
1 parent cb69084 commit e8a1fdb

File tree

6 files changed

+106
-18
lines changed

6 files changed

+106
-18
lines changed

src/main/java/ca/trackerforce/DotUtils.java

Lines changed: 65 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import ca.trackerforce.path.DotPathFactory;
44

5+
import java.lang.reflect.Array;
56
import java.util.Collections;
67
import java.util.List;
78
import java.util.Map;
@@ -33,11 +34,13 @@ public static List<String> parsePaths(String path) {
3334
* @throws ClassCastException if the property is not a map
3435
*/
3536
public static Map<String, Object> mapFrom(Map<String, Object> source, String property) {
36-
if (isInvalid(source, property)) {
37+
Object result = getObjectFromSource(source, property);
38+
39+
if (!(result instanceof Map)) {
3740
return Collections.emptyMap();
3841
}
3942

40-
return (Map<String, Object>) source.get(property);
43+
return (Map<String, Object>) result;
4144
}
4245

4346
/**
@@ -49,31 +52,84 @@ public static Map<String, Object> mapFrom(Map<String, Object> source, String pro
4952
* @throws ClassCastException if the property is not a list of maps
5053
*/
5154
public static List<Map<String, Object>> listFrom(Map<String, Object> source, String property) {
52-
if (isInvalid(source, property)) {
55+
Object result = getObjectFromSource(source, property);
56+
57+
if (!(result instanceof List)) {
5358
return Collections.emptyList();
5459
}
5560

56-
return (List<Map<String, Object>>) source.get(property);
61+
return (List<Map<String, Object>>) result;
62+
}
63+
64+
/**
65+
* Extracts a typed list from the source map based on the specified property.
66+
*
67+
* @param source the source map
68+
* @param property the property to extract or a dot-notated path for nested properties
69+
* @return the extracted list of maps or an empty list if not found
70+
* @throws ClassCastException if the property is not a list of maps
71+
*/
72+
public static <T> List<T> listFrom(Map<String, Object> source, String property, Class<T> clazz) {
73+
Object result = getObjectFromSource(source, property);
74+
75+
if (!(result instanceof List) || ((List<?>) result).isEmpty() || !clazz.isInstance(((List<?>) result).get(0))) {
76+
return Collections.emptyList();
77+
}
78+
79+
return (List<T>) result;
5780
}
5881

5982
/**
6083
* Extracts a list of objects from the source map based on the specified property.
6184
*
6285
* @param source the source map
63-
* @param property the property to extract
86+
* @param property the property to extract or a dot-notated path for nested properties
6487
* @return the extracted list of objects or an empty list if not found
6588
* @throws ClassCastException if the property is not a list of objects
6689
*/
6790
public static Object[] arrayFrom(Map<String, Object> source, String property) {
68-
if (isInvalid(source, property)) {
91+
Object result = getObjectFromSource(source, property);
92+
93+
if (result == null || !result.getClass().isArray()) {
6994
return new Object[0];
7095
}
7196

72-
return (Object[]) source.get(property);
97+
return convertToObjectArray(result);
7398
}
7499

75-
private static boolean isInvalid(Map<String, Object> source, String property) {
76-
return source == null || property == null || property.isEmpty() || !source.containsKey(property);
100+
private static Object getObjectFromSource(Map<String, Object> source, String property) {
101+
if (property.contains(".")) {
102+
String[] keys = property.split("\\.");
103+
104+
for (int i = 0; i < keys.length - 1; i++) {
105+
source = (Map<String, Object>) source.get(keys[i]);
106+
}
107+
108+
return source.get(keys[keys.length - 1]);
109+
}
110+
111+
if (source == null || !source.containsKey(property)) {
112+
return null;
113+
}
114+
115+
return source.get(property);
116+
}
117+
118+
private static Object[] convertToObjectArray(Object array) {
119+
Class<?> componentType = array.getClass().getComponentType();
120+
121+
if (!componentType.isPrimitive()) {
122+
return (Object[]) array;
123+
}
124+
125+
int length = Array.getLength(array);
126+
Object[] result = new Object[length];
127+
128+
for (int i = 0; i < length; i++) {
129+
result[i] = Array.get(array, i);
130+
}
131+
132+
return result;
77133
}
78134
}
79135

src/main/java/ca/trackerforce/path/PathCommon.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,11 @@ protected <T> Object getPropertyValue(T source, String propertyName) {
100100
return getterResult;
101101
}
102102

103+
// Try mapping method for Map instances
104+
if (source instanceof Map<?, ?> map && map.containsKey(propertyName)) {
105+
return map.get(propertyName);
106+
}
107+
103108
// Fall back to direct field access
104109
return tryDirectFieldAccess(source, propertyName, clazz);
105110
} catch (Exception e) {

src/test/java/ca/trackerforce/DotUtilsTest.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package ca.trackerforce;
22

3-
import ca.trackerforce.fixture.record.UserDetail;
43
import org.junit.jupiter.api.Test;
54
import org.junit.jupiter.params.ParameterizedTest;
65
import org.junit.jupiter.params.provider.Arguments;
@@ -17,7 +16,7 @@ class DotUtilsTest {
1716

1817
static Stream<Arguments> userDetailProvider() {
1918
return Stream.of(
20-
Arguments.of("Record type", UserDetail.of()),
19+
Arguments.of("Record type", ca.trackerforce.fixture.record.UserDetail.of()),
2120
Arguments.of("Class type", ca.trackerforce.fixture.clazz.UserDetail.of())
2221
);
2322
}
@@ -27,13 +26,19 @@ static Stream<Arguments> userDetailProvider() {
2726
void shouldReturnSelectedArrayProperties(String implementation, Object userDetail) {
2827
// When
2928
var result = dotPathQL.toMap(userDetail);
30-
var roles = DotUtils.arrayFrom(result, "roles");
29+
var roles = DotUtils.arrayFrom(result, "roles"); // simple array
30+
var locationsHomeCoordinates = DotUtils.arrayFrom(result, "locations.home.coordinates"); // nested array
3131

3232
// Then
3333
assertNotNull(roles);
3434
assertEquals(2, roles.length);
3535
assertEquals("USER", roles[0]);
3636
assertEquals("ADMIN", roles[1]);
37+
38+
assertNotNull(locationsHomeCoordinates);
39+
assertEquals(2, locationsHomeCoordinates.length);
40+
assertEquals(39, locationsHomeCoordinates[0]);
41+
assertEquals(89, locationsHomeCoordinates[1]);
3742
}
3843

3944
@ParameterizedTest(name = "{0}")
@@ -42,10 +47,16 @@ void shouldReturnSelectedListProperties(String implementation, Object userDetail
4247
// When
4348
var result = dotPathQL.toMap(userDetail);
4449
var occupations = DotUtils.listFrom(result, "occupations");
50+
var locationsWorkNumbers = DotUtils.listFrom(result, "locations.work.numbers", Integer.class);
4551

4652
// Then
4753
assertNotNull(occupations);
4854
assertEquals(2, occupations.size());
55+
56+
assertNotNull(locationsWorkNumbers);
57+
assertEquals(5, locationsWorkNumbers.size());
58+
assertEquals(11, locationsWorkNumbers.get(0));
59+
assertEquals(15, locationsWorkNumbers.get(4));
4960
}
5061

5162
@ParameterizedTest(name = "{0}")

src/test/java/ca/trackerforce/ExcludeTypeClassRecordTest.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package ca.trackerforce;
22

3-
import ca.trackerforce.fixture.record.UserDetail;
43
import org.junit.jupiter.api.Test;
54
import org.junit.jupiter.params.ParameterizedTest;
65
import org.junit.jupiter.params.provider.Arguments;
@@ -17,7 +16,7 @@ class ExcludeTypeClassRecordTest {
1716

1817
static Stream<Arguments> userDetailProvider() {
1918
return Stream.of(
20-
Arguments.of("Record type", UserDetail.of()),
19+
Arguments.of("Record type", ca.trackerforce.fixture.record.UserDetail.of()),
2120
Arguments.of("Class type", ca.trackerforce.fixture.clazz.UserDetail.of())
2221
);
2322
}

src/test/java/ca/trackerforce/FilterTypeClassRecordTest.java

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package ca.trackerforce;
22

3-
import ca.trackerforce.fixture.record.UserDetail;
43
import org.junit.jupiter.api.Test;
54
import org.junit.jupiter.params.ParameterizedTest;
65
import org.junit.jupiter.params.provider.Arguments;
@@ -17,7 +16,7 @@ class FilterTypeClassRecordTest {
1716

1817
static Stream<Arguments> userDetailProvider() {
1918
return Stream.of(
20-
Arguments.of("Record type", UserDetail.of()),
19+
Arguments.of("Record type", ca.trackerforce.fixture.record.UserDetail.of()),
2120
Arguments.of("Class type", ca.trackerforce.fixture.clazz.UserDetail.of())
2221
);
2322
}
@@ -185,6 +184,24 @@ void shouldReturnFilteredObjectUsingNestedGroupedPaths(String implementation, Ob
185184
assertEquals("Springfield", workLocation.get("city"));
186185
}
187186

187+
@ParameterizedTest(name = "{0}")
188+
@MethodSource("userDetailProvider")
189+
void shouldReturnFilteredMapAttributes(String implementation, Object userDetail) {
190+
// When
191+
var source = dotPathQL.toMap(userDetail); // Convert to Map for testing
192+
var result = dotPathQL.filter(source, List.of(
193+
"username",
194+
"address.street",
195+
"orders.products.name"
196+
));
197+
198+
// Then
199+
assertEquals(3, result.size());
200+
assertEquals("john_doe", result.get("username"));
201+
assertEquals(1, DotUtils.mapFrom(result, "address").size());
202+
assertEquals(2, DotUtils.listFrom(result, "orders").size());
203+
}
204+
188205
@ParameterizedTest(name = "{0}")
189206
@MethodSource("userDetailProvider")
190207
void shouldReturnEmptyResultInvalidGroupedPaths(String implementation, Object userDetail) {
@@ -204,4 +221,5 @@ void shouldReturnEmptyMapWhenSourceIsNull() {
204221
assertNotNull(result);
205222
assertTrue(result.isEmpty());
206223
}
224+
207225
}

src/test/java/ca/trackerforce/PrintTypeClassRecordTest.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package ca.trackerforce;
22

3-
import ca.trackerforce.fixture.record.UserDetail;
43
import org.junit.jupiter.api.Test;
54
import org.junit.jupiter.params.ParameterizedTest;
65
import org.junit.jupiter.params.provider.Arguments;
@@ -18,7 +17,7 @@ class PrintTypeClassRecordTest {
1817

1918
static Stream<Arguments> userDetailProvider() {
2019
return Stream.of(
21-
Arguments.of("Record type", UserDetail.of()),
20+
Arguments.of("Record type", ca.trackerforce.fixture.record.UserDetail.of()),
2221
Arguments.of("Class type", ca.trackerforce.fixture.clazz.UserDetail.of())
2322
);
2423
}

0 commit comments

Comments
 (0)