diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index a501951a..42bf91fd 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,6 +1,6 @@ # These are supported funding model platforms -github: perplexhub # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username diff --git a/rsql-common/src/main/java/io/github/perplexhub/rsql/RSQLCommonSupport.java b/rsql-common/src/main/java/io/github/perplexhub/rsql/RSQLCommonSupport.java index 25db8493..815358b7 100644 --- a/rsql-common/src/main/java/io/github/perplexhub/rsql/RSQLCommonSupport.java +++ b/rsql-common/src/main/java/io/github/perplexhub/rsql/RSQLCommonSupport.java @@ -28,6 +28,7 @@ public class RSQLCommonSupport { private @Getter static final Map valueTypeMap = new ConcurrentHashMap<>(); private @Getter static final Map, List> propertyWhitelist = new ConcurrentHashMap<>(); private @Getter static final Map, List> propertyBlacklist = new ConcurrentHashMap<>(); + private @Getter static final Map, Map>> fieldTransformers = new ConcurrentHashMap<>(); private @Getter static final ConfigurableConversionService conversionService = new DefaultConversionService(); public RSQLCommonSupport() { @@ -51,6 +52,7 @@ protected void init() { RSQLVisitorBase.setPropertyRemapping(getPropertyRemapping()); RSQLVisitorBase.setGlobalPropertyWhitelist(getPropertyWhitelist()); RSQLVisitorBase.setGlobalPropertyBlacklist(getPropertyBlacklist()); + RSQLVisitorBase.setFieldTransformers(getFieldTransformers()); RSQLVisitorBase.setDefaultConversionService(getConversionService()); log.info("RSQLCommonSupport {} is initialized", getVersion()); } @@ -127,6 +129,14 @@ public static void addEntityAttributeTypeMap(Class valueClass, Class mappedClass } } + public static void addFieldTransformer(Class entityClass, String fieldName, Function transformer) { + log.info("Adding field transformer for {}.{}", entityClass, fieldName); + if (entityClass != null && fieldName != null && transformer != null) { + fieldTransformers.computeIfAbsent(entityClass, k -> new ConcurrentHashMap<>()) + .put(fieldName, transformer); + } + } + protected String getVersion() { try { Properties prop = new Properties(); diff --git a/rsql-common/src/main/java/io/github/perplexhub/rsql/RSQLVisitorBase.java b/rsql-common/src/main/java/io/github/perplexhub/rsql/RSQLVisitorBase.java index d8ec4dfc..743c5c8c 100644 --- a/rsql-common/src/main/java/io/github/perplexhub/rsql/RSQLVisitorBase.java +++ b/rsql-common/src/main/java/io/github/perplexhub/rsql/RSQLVisitorBase.java @@ -3,9 +3,9 @@ import java.lang.reflect.*; import java.sql.Timestamp; import java.time.*; -import java.time.format.DateTimeParseException; import java.util.*; import java.util.Map.Entry; +import java.util.function.Function; import jakarta.persistence.EntityManager; import jakarta.persistence.metamodel.Attribute; @@ -16,7 +16,6 @@ import lombok.Getter; import org.hibernate.metamodel.model.domain.ManagedDomainType; import org.hibernate.metamodel.model.domain.PersistentAttribute; -import org.springframework.core.convert.ConversionFailedException; import org.springframework.core.convert.support.ConfigurableConversionService; import org.springframework.orm.jpa.vendor.Database; import org.springframework.util.StringUtils; @@ -37,6 +36,7 @@ public abstract class RSQLVisitorBase implements RSQLVisitor { protected static volatile @Setter Map, Map> propertyRemapping; protected static volatile @Setter Map, List> globalPropertyWhitelist; protected static volatile @Setter Map, List> globalPropertyBlacklist; + protected static volatile @Setter Map, Map>> fieldTransformers; protected static volatile @Setter ConfigurableConversionService defaultConversionService; protected @Setter Map, List> propertyWhitelist; @@ -77,6 +77,25 @@ public Map, Map> getPropertyRemapping() { protected Object convert(String source, Class targetType) { log.debug("convert(source:{},targetType:{})", source, targetType); + if (source == null) { + return null; + } + + // Check for field-specific transformer first + if (fieldTransformers != null) { + Map> entityTransformers = fieldTransformers.get(targetType); + if (entityTransformers != null) { + Function transformer = entityTransformers.get(source); + if (transformer != null) { + try { + return transformer.apply(source); + } catch (Exception e) { + log.warn("Failed to apply field transformer for {}.{}", targetType, source, e); + } + } + } + } + Object object = null; try { if (defaultConversionService.canConvert(String.class, targetType)) { diff --git a/rsql-common/src/test/java/io/github/perplexhub/rsql/model/User.java b/rsql-common/src/test/java/io/github/perplexhub/rsql/model/User.java index ca4db222..fb30d9cb 100644 --- a/rsql-common/src/test/java/io/github/perplexhub/rsql/model/User.java +++ b/rsql-common/src/test/java/io/github/perplexhub/rsql/model/User.java @@ -22,6 +22,12 @@ public class User { private String name; + private String number; + + private String email; + + private String phone; + @ManyToOne @JoinColumn(name = "companyId", referencedColumnName = "id") private Company company; diff --git a/rsql-jpa/src/test/java/io/github/perplexhub/rsql/RSQLJPASupportTest.java b/rsql-jpa/src/test/java/io/github/perplexhub/rsql/RSQLJPASupportTest.java index 6d08712e..9a6c9396 100644 --- a/rsql-jpa/src/test/java/io/github/perplexhub/rsql/RSQLJPASupportTest.java +++ b/rsql-jpa/src/test/java/io/github/perplexhub/rsql/RSQLJPASupportTest.java @@ -5,9 +5,12 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; +import static org.mockito.Mockito.verify; import java.sql.Timestamp; import java.text.ParseException; @@ -17,6 +20,8 @@ import java.time.ZoneOffset; import java.util.*; +import cz.jirutka.rsql.parser.RSQLParser; +import cz.jirutka.rsql.parser.ast.Node; import io.github.perplexhub.rsql.custom.CustomType; import io.github.perplexhub.rsql.model.Project; import io.github.perplexhub.rsql.model.AdminProject; @@ -28,6 +33,7 @@ import io.github.perplexhub.rsql.repository.jpa.ProjectRepository; import io.github.perplexhub.rsql.repository.jpa.custom.CustomTypeRepository; import io.github.perplexhub.rsql.custom.EntityWithCustomType; +import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.Expression; import jakarta.persistence.criteria.JoinType; import jakarta.persistence.criteria.Predicate; @@ -1463,6 +1469,52 @@ void testSearchBySubtypeAttribute() { Assertions.assertThat(result).hasSize(1); } + @Test + void testNumberFieldTransformer() { + RSQLCommonSupport.addFieldTransformer(User.class, "number", value -> value.replaceAll("[^0-9]", "")); + String rsql = "number=like='123????'"; + List users = userRepository.findAll(toSpecification(rsql)); + for (User user : users) + System.out.println(user); + } + + @Test + void testEmailFieldTransformer() { + RSQLCommonSupport.addFieldTransformer(User.class, "email", value -> value.toLowerCase()); + String rsql = "email=like='TEST@EXAMPLE.COM'"; + List users = userRepository.findAll(toSpecification(rsql)); + for (User user : users) + System.out.println(user); + } + + @Test + void testPhoneFieldTransformer() { + RSQLCommonSupport.addFieldTransformer(User.class, "phone", value -> value.replaceAll("[^0-9+]", "")); + String rsql = "phone=like='+1 (555) 123-4567'"; + List users = userRepository.findAll(toSpecification(rsql)); + for (User user : users) + System.out.println(user); + } + + @Test + void testMultipleTransformers() { + RSQLCommonSupport.addFieldTransformer(User.class, "number", value -> value.replaceAll("[^0-9]", "")); + RSQLCommonSupport.addFieldTransformer(User.class, "email", value -> value.toLowerCase()); + String rsql = "number=like='123????' and email=like='TEST@EXAMPLE.COM'"; + List users = userRepository.findAll(toSpecification(rsql)); + for (User user : users) + System.out.println(user); + } + + @Test + void testTransformerWithInvalidValue() { + String rsql = "number=like='abc'"; + RSQLCommonSupport.addFieldTransformer(User.class, "number", value -> value.replaceAll("[^0-9]", "")); + List users = userRepository.findAll(toSpecification(rsql)); + for (User user : users) + System.out.println(user); + } + @BeforeEach void setUp() { getPropertyWhitelist().clear();