Skip to content

Commit 862d6a1

Browse files
committed
Apply custom function for field entity #177
1 parent c01825e commit 862d6a1

File tree

4 files changed

+201
-2
lines changed

4 files changed

+201
-2
lines changed

rsql-common/src/main/java/io/github/perplexhub/rsql/RSQLCommonSupport.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public class RSQLCommonSupport {
2828
private @Getter static final Map<Class, Class> valueTypeMap = new ConcurrentHashMap<>();
2929
private @Getter static final Map<Class<?>, List<String>> propertyWhitelist = new ConcurrentHashMap<>();
3030
private @Getter static final Map<Class<?>, List<String>> propertyBlacklist = new ConcurrentHashMap<>();
31+
private @Getter static final Map<Class<?>, Map<String, Function<String, ?>>> fieldTransformers = new ConcurrentHashMap<>();
3132
private @Getter static final ConfigurableConversionService conversionService = new DefaultConversionService();
3233

3334
public RSQLCommonSupport() {
@@ -51,6 +52,7 @@ protected void init() {
5152
RSQLVisitorBase.setPropertyRemapping(getPropertyRemapping());
5253
RSQLVisitorBase.setGlobalPropertyWhitelist(getPropertyWhitelist());
5354
RSQLVisitorBase.setGlobalPropertyBlacklist(getPropertyBlacklist());
55+
RSQLVisitorBase.setFieldTransformers(getFieldTransformers());
5456
RSQLVisitorBase.setDefaultConversionService(getConversionService());
5557
log.info("RSQLCommonSupport {} is initialized", getVersion());
5658
}
@@ -127,6 +129,14 @@ public static void addEntityAttributeTypeMap(Class valueClass, Class mappedClass
127129
}
128130
}
129131

132+
public static <T> void addFieldTransformer(Class<?> entityClass, String fieldName, Function<String, ?> transformer) {
133+
log.info("Adding field transformer for {}.{}", entityClass, fieldName);
134+
if (entityClass != null && fieldName != null && transformer != null) {
135+
fieldTransformers.computeIfAbsent(entityClass, k -> new ConcurrentHashMap<>())
136+
.put(fieldName, transformer);
137+
}
138+
}
139+
130140
protected String getVersion() {
131141
try {
132142
Properties prop = new Properties();

rsql-common/src/main/java/io/github/perplexhub/rsql/RSQLVisitorBase.java

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
import java.lang.reflect.*;
44
import java.sql.Timestamp;
55
import java.time.*;
6-
import java.time.format.DateTimeParseException;
76
import java.util.*;
87
import java.util.Map.Entry;
8+
import java.util.function.Function;
99

1010
import jakarta.persistence.EntityManager;
1111
import jakarta.persistence.metamodel.Attribute;
@@ -16,7 +16,6 @@
1616
import lombok.Getter;
1717
import org.hibernate.metamodel.model.domain.ManagedDomainType;
1818
import org.hibernate.metamodel.model.domain.PersistentAttribute;
19-
import org.springframework.core.convert.ConversionFailedException;
2019
import org.springframework.core.convert.support.ConfigurableConversionService;
2120
import org.springframework.orm.jpa.vendor.Database;
2221
import org.springframework.util.StringUtils;
@@ -37,6 +36,7 @@ public abstract class RSQLVisitorBase<R, A> implements RSQLVisitor<R, A> {
3736
protected static volatile @Setter Map<Class<?>, Map<String, String>> propertyRemapping;
3837
protected static volatile @Setter Map<Class<?>, List<String>> globalPropertyWhitelist;
3938
protected static volatile @Setter Map<Class<?>, List<String>> globalPropertyBlacklist;
39+
protected static volatile @Setter Map<Class<?>, Map<String, Function<String, ?>>> fieldTransformers;
4040
protected static volatile @Setter ConfigurableConversionService defaultConversionService;
4141

4242
protected @Setter Map<Class<?>, List<String>> propertyWhitelist;
@@ -77,6 +77,25 @@ public Map<Class<?>, Map<String, String>> getPropertyRemapping() {
7777
protected Object convert(String source, Class targetType) {
7878
log.debug("convert(source:{},targetType:{})", source, targetType);
7979

80+
if (source == null) {
81+
return null;
82+
}
83+
84+
// Check for field-specific transformer first
85+
if (fieldTransformers != null) {
86+
Map<String, Function<String, ?>> entityTransformers = fieldTransformers.get(targetType);
87+
if (entityTransformers != null) {
88+
Function<String, ?> transformer = entityTransformers.get(source);
89+
if (transformer != null) {
90+
try {
91+
return transformer.apply(source);
92+
} catch (Exception e) {
93+
log.warn("Failed to apply field transformer for {}.{}", targetType, source, e);
94+
}
95+
}
96+
}
97+
}
98+
8099
Object object = null;
81100
try {
82101
if (defaultConversionService.canConvert(String.class, targetType)) {

rsql-common/src/test/java/io/github/perplexhub/rsql/model/User.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ public class User {
2222

2323
private String name;
2424

25+
private String number;
26+
27+
private String email;
28+
29+
private String phone;
30+
2531
@ManyToOne
2632
@JoinColumn(name = "companyId", referencedColumnName = "id")
2733
private Company company;
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
package io.github.perplexhub.rsql;
2+
3+
import static org.junit.jupiter.api.Assertions.*;
4+
import static org.mockito.Mockito.*;
5+
6+
import java.util.Collections;
7+
import java.util.Map;
8+
9+
import io.github.perplexhub.rsql.model.User;
10+
import jakarta.persistence.EntityManager;
11+
import jakarta.persistence.TypedQuery;
12+
import jakarta.persistence.criteria.CriteriaBuilder;
13+
import jakarta.persistence.criteria.CriteriaQuery;
14+
import jakarta.persistence.criteria.Predicate;
15+
import jakarta.persistence.criteria.Root;
16+
17+
import org.junit.jupiter.api.BeforeEach;
18+
import org.junit.jupiter.api.Test;
19+
import org.junit.jupiter.api.extension.ExtendWith;
20+
import org.mockito.Mock;
21+
import org.mockito.junit.jupiter.MockitoExtension;
22+
23+
import cz.jirutka.rsql.parser.RSQLParser;
24+
import cz.jirutka.rsql.parser.ast.Node;
25+
26+
@ExtendWith(MockitoExtension.class)
27+
class RSQLFieldTransformerTest {
28+
29+
@Mock
30+
private EntityManager entityManager;
31+
32+
@Mock
33+
private CriteriaBuilder criteriaBuilder;
34+
35+
@Mock
36+
private CriteriaQuery<User> criteriaQuery;
37+
38+
@Mock
39+
private Root<User> root;
40+
41+
@Mock
42+
private TypedQuery<User> typedQuery;
43+
44+
private Map<String, String> propertyPathMapper;
45+
46+
@BeforeEach
47+
void setUp() {
48+
propertyPathMapper = Collections.emptyMap();
49+
50+
// Setup field transformers
51+
RSQLCommonSupport.addFieldTransformer(User.class, "number",
52+
value -> value.replaceAll("[^0-9]", ""));
53+
54+
RSQLCommonSupport.addFieldTransformer(User.class, "email",
55+
value -> value.toLowerCase());
56+
57+
RSQLCommonSupport.addFieldTransformer(User.class, "phone",
58+
value -> value.replaceAll("[^0-9+]", ""));
59+
}
60+
61+
@Test
62+
void testNumberFieldTransformer() {
63+
// Given
64+
String rsqlQuery = "number=like='123????'";
65+
when(entityManager.getCriteriaBuilder()).thenReturn(criteriaBuilder);
66+
when(entityManager.createQuery(any(CriteriaQuery.class))).thenReturn(typedQuery);
67+
when(criteriaQuery.from(User.class)).thenReturn(root);
68+
when(root.get("number")).thenReturn(mock());
69+
when(criteriaBuilder.like(any(), eq("%123%"))).thenReturn(mock());
70+
71+
// When
72+
Node rootNode = new RSQLParser().parse(rsqlQuery);
73+
RSQLJPAPredicateConverter converter = new RSQLJPAPredicateConverter(criteriaBuilder, propertyPathMapper);
74+
Predicate predicate = rootNode.accept(converter, root);
75+
76+
// Then
77+
assertNotNull(predicate);
78+
verify(criteriaBuilder).like(any(), eq("%123%"));
79+
}
80+
81+
@Test
82+
void testEmailFieldTransformer() {
83+
// Given
84+
String rsqlQuery = "email=like='[email protected]'";
85+
when(entityManager.getCriteriaBuilder()).thenReturn(criteriaBuilder);
86+
when(entityManager.createQuery(any(CriteriaQuery.class))).thenReturn(typedQuery);
87+
when(criteriaQuery.from(User.class)).thenReturn(root);
88+
when(root.get("email")).thenReturn(mock());
89+
when(criteriaBuilder.like(any(), eq("%[email protected]%"))).thenReturn(mock());
90+
91+
// When
92+
Node rootNode = new RSQLParser().parse(rsqlQuery);
93+
RSQLJPAPredicateConverter converter = new RSQLJPAPredicateConverter(criteriaBuilder, propertyPathMapper);
94+
Predicate predicate = rootNode.accept(converter, root);
95+
96+
// Then
97+
assertNotNull(predicate);
98+
verify(criteriaBuilder).like(any(), eq("%[email protected]%"));
99+
}
100+
101+
@Test
102+
void testPhoneFieldTransformer() {
103+
// Given
104+
String rsqlQuery = "phone=like='+1 (555) 123-4567'";
105+
when(entityManager.getCriteriaBuilder()).thenReturn(criteriaBuilder);
106+
when(entityManager.createQuery(any(CriteriaQuery.class))).thenReturn(typedQuery);
107+
when(criteriaQuery.from(User.class)).thenReturn(root);
108+
when(root.get("phone")).thenReturn(mock());
109+
when(criteriaBuilder.like(any(), eq("%+15551234567%"))).thenReturn(mock());
110+
111+
// When
112+
Node rootNode = new RSQLParser().parse(rsqlQuery);
113+
RSQLJPAPredicateConverter converter = new RSQLJPAPredicateConverter(criteriaBuilder, propertyPathMapper);
114+
Predicate predicate = rootNode.accept(converter, root);
115+
116+
// Then
117+
assertNotNull(predicate);
118+
verify(criteriaBuilder).like(any(), eq("%+15551234567%"));
119+
}
120+
121+
@Test
122+
void testMultipleTransformers() {
123+
// Given
124+
String rsqlQuery = "number=like='123????' and email=like='[email protected]'";
125+
when(entityManager.getCriteriaBuilder()).thenReturn(criteriaBuilder);
126+
when(entityManager.createQuery(any(CriteriaQuery.class))).thenReturn(typedQuery);
127+
when(criteriaQuery.from(User.class)).thenReturn(root);
128+
when(root.get("number")).thenReturn(mock());
129+
when(root.get("email")).thenReturn(mock());
130+
when(criteriaBuilder.like(any(), eq("%123%"))).thenReturn(mock());
131+
when(criteriaBuilder.like(any(), eq("%[email protected]%"))).thenReturn(mock());
132+
when(criteriaBuilder.and(any(), any())).thenReturn(mock());
133+
134+
// When
135+
Node rootNode = new RSQLParser().parse(rsqlQuery);
136+
RSQLJPAPredicateConverter converter = new RSQLJPAPredicateConverter(criteriaBuilder, propertyPathMapper);
137+
Predicate predicate = rootNode.accept(converter, root);
138+
139+
// Then
140+
assertNotNull(predicate);
141+
verify(criteriaBuilder).like(any(), eq("%123%"));
142+
verify(criteriaBuilder).like(any(), eq("%[email protected]%"));
143+
}
144+
145+
@Test
146+
void testTransformerWithInvalidValue() {
147+
// Given
148+
String rsqlQuery = "number=like='abc'";
149+
when(entityManager.getCriteriaBuilder()).thenReturn(criteriaBuilder);
150+
when(entityManager.createQuery(any(CriteriaQuery.class))).thenReturn(typedQuery);
151+
when(criteriaQuery.from(User.class)).thenReturn(root);
152+
when(root.get("number")).thenReturn(mock());
153+
when(criteriaBuilder.like(any(), eq("%%"))).thenReturn(mock());
154+
155+
// When
156+
Node rootNode = new RSQLParser().parse(rsqlQuery);
157+
RSQLJPAPredicateConverter converter = new RSQLJPAPredicateConverter(criteriaBuilder, propertyPathMapper);
158+
Predicate predicate = rootNode.accept(converter, root);
159+
160+
// Then
161+
assertNotNull(predicate);
162+
verify(criteriaBuilder).like(any(), eq("%%"));
163+
}
164+
}

0 commit comments

Comments
 (0)