Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/kumuluzee-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
java-version: ${{ matrix.java-version }}

- name: Cache Maven packages
uses: actions/cache@v2
uses: actions/cache@v4
with:
path: ~/.m2
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
Expand All @@ -34,7 +34,7 @@ jobs:
run: mvn --batch-mode test

publish:
name: Publish to OSSRH
name: Publish to Maven Central
runs-on: 'ubuntu-latest'

needs: test
Expand Down Expand Up @@ -62,9 +62,9 @@ jobs:
GPG_PUB: ${{ secrets.OSSRH_GPG_PUB }}
run: 'printf "$GPG_KEY" | base64 --decode > "$GITHUB_WORKSPACE/gpg.key" && printf "$GPG_PUB" | base64 --decode > "$GITHUB_WORKSPACE/gpg.pub" && gpg --batch --import "$GITHUB_WORKSPACE/gpg.pub" "$GITHUB_WORKSPACE/gpg.key"'

- name: Publish to OSSRH
- name: Publish to Maven Central
env:
OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }}
OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }}
MAVEN_CENTRAL_USER: ${{ secrets.MAVEN_CENTRAL_USER }}
MAVEN_CENTRAL_TOKEN: ${{ secrets.MAVEN_CENTRAL_TOKEN }}
GPG_PASSPHRASE: ${{ secrets.OSSRH_GPG_PASSPHRASE }}
run: mvn --batch-mode source:jar javadoc:jar deploy -Pdeploy -DskipTests=true --settings ./settings.xml;
2 changes: 1 addition & 1 deletion core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<parent>
<artifactId>kumuluzee-rest</artifactId>
<groupId>com.kumuluz.ee.rest</groupId>
<version>3.1.0-SNAPSHOT</version>
<version>3.1.0</version>
</parent>

<name>KumuluzEE REST Core</name>
Expand Down
73 changes: 73 additions & 0 deletions core/src/main/java/com/kumuluz/ee/rest/beans/QueryParameters.java
Original file line number Diff line number Diff line change
Expand Up @@ -109,12 +109,72 @@ public void setFields(List<String> fields) {
* @deprecated Will be removed in future releases. Use
* {@link #addFilterExpression(FilterExpressionOperation, QueryFilterExpression)} ()} instead.
*/
@Deprecated
public void addFilter(QueryFilter filter) {
if (filter != null) {
getFilters().add(filter);
}
}

/**
* Removes all filter parameters with the specified field name from the filter expression tree.
* This method rebuilds the filter expression tree, excluding any filters that match the given field.
* If all filters are removed, the filter expression is set to null.
*
* @param field the field name of the filters to remove
*/
public void removeFilterParameter(String field) {
if (field == null) {
return;
}

if (filterExpression != null) {
filterExpression = removeFromExpression(filterExpression, field);
}
}

/**
* Recursively removes all filters with the specified field name from the expression tree.
* Returns null if the entire subtree should be removed.
*
* @param expression the expression to process
* @param field the field name to remove
* @return the rebuilt expression without the specified field, or null if removed
*/
private QueryFilterExpression removeFromExpression(QueryFilterExpression expression, String field) {
if (expression == null) {
return null;
}

// If this is a leaf node with the matching field, remove it
if (expression.isLeaf()) {
if (expression.value() != null && field.equals(expression.value().getField())) {
return null; // Remove this node
}
return expression; // Keep this node
}

// If this is a branch node, recursively process left and right
QueryFilterExpression newLeft = removeFromExpression(expression.left(), field);
QueryFilterExpression newRight = removeFromExpression(expression.right(), field);

// If both sides are removed, remove this entire branch
if (newLeft == null && newRight == null) {
return null;
}

// If one side is removed, return the other side (collapse the tree)
if (newLeft == null) {
return newRight;
}
if (newRight == null) {
return newLeft;
}

// Both sides still exist, reconstruct the branch with the same operation
return new QueryFilterExpression(expression.operation(), newLeft, newRight);
}

/**
* @deprecated Will be removed in future releases. Use {@link #setFilterExpression(QueryFilterExpression)} ()}
* instead.
Expand Down Expand Up @@ -145,6 +205,19 @@ public QueryFilterExpression getFilterExpression() {
return filterExpression;
}

/**
* Safely returns all filter values from the filter expression.
* Returns an empty list if filterExpression is null.
*
* @return list of QueryFilter values, never null
*/
public List<QueryFilter> getFilterValues() {
if (filterExpression == null) {
return new ArrayList<>();
}
return filterExpression.getAllValues();
}

public void setFilterExpression(QueryFilterExpression filterExpression) {
this.filterExpression = filterExpression;
}
Expand Down
6 changes: 5 additions & 1 deletion core/src/main/java/com/kumuluz/ee/rest/utils/JPAUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,10 @@ private static CriteriaWhereQuery createWhereQueryInternal(EntityManager em, Cri

private static Predicate createWhereQueryInternal(EntityManager em, CriteriaBuilder cb, Root<?> r, AtomicBoolean containsToManyAtomic, QueryFilterExpression filterExpression, Map<String, From> fieldJoins) {

if (filterExpression == null) {
return null;
}

if (filterExpression.isLeaf()) {
QueryFilter f = filterExpression.value();

Expand Down Expand Up @@ -1068,7 +1072,7 @@ private static Object getValueForPath(Path path, String value) {

try {

if (c.equals(Date.class)) {
if (Date.class.isAssignableFrom(c)) {
return Date.from(ZonedDateTime.parse(value).toInstant());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,11 @@ private List<String> buildFields(String value) {
private QueryFilterExpression buildFilterExpression(String key, String value) {
log.finest("Building filter string: " + value);

// Skip filters with empty values (e.g., "field:like:" with no value after operator)
if (value == null || value.isEmpty() || value.matches(".*:\\s*$")) {
return null;
}

QueryFilterExpressionParser parser = Parboiled.createParser(QueryFilterExpressionParser.class, key);

QueryFilterExpression filterExpression;
Expand Down
227 changes: 227 additions & 0 deletions core/src/test/java/com/kumuluz/ee/rest/test/QueryParametersTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
package com.kumuluz.ee.rest.test;

import com.kumuluz.ee.rest.beans.QueryFilter;
import com.kumuluz.ee.rest.beans.QueryFilterExpression;
import com.kumuluz.ee.rest.beans.QueryParameters;
import com.kumuluz.ee.rest.enums.FilterExpressionOperation;
import com.kumuluz.ee.rest.enums.FilterOperation;
import org.junit.Assert;
import org.junit.Test;

import java.util.List;

/**
* Tests for QueryParameters methods.
*
* @author cen1
*/
public class QueryParametersTest {

@Test
public void testRemoveFilterParameterSingleFilter() {
// Create query with a single filter using new API
QueryParameters query = QueryParameters.query("filter=username:eq:test").build();

Assert.assertEquals(1, query.getFilterValues().size());

// Remove the filter
query.removeFilterParameter("username");

// Verify filter is removed
Assert.assertEquals(0, query.getFilterValues().size());
Assert.assertNull(query.getFilterExpression());
}

@Test
public void testRemoveFilterParameterMultipleFilters() {
// Create query with multiple filters on different fields using new API
QueryParameters query = QueryParameters
.query("filter=username:eq:test AND email:like:test@example.com AND age:gt:18")
.build();

Assert.assertEquals(3, query.getFilterValues().size());

// Remove one filter
query.removeFilterParameter("username");

// Verify only the specified filter is removed
Assert.assertEquals(2, query.getFilterValues().size());
List<QueryFilter> remainingFilters = query.getFilterValues();
Assert.assertTrue(remainingFilters.stream().noneMatch(f -> "username".equals(f.getField())));
Assert.assertTrue(remainingFilters.stream().anyMatch(f -> "email".equals(f.getField())));
Assert.assertTrue(remainingFilters.stream().anyMatch(f -> "age".equals(f.getField())));
}

@Test
public void testRemoveFilterParameterMultipleSameField() {
// Create query with multiple filters on the same field using new API
QueryParameters query = QueryParameters
.query("filter=status:eq:active OR status:eq:pending")
.build();

Assert.assertEquals(2, query.getFilterValues().size());

// Remove all filters for this field
query.removeFilterParameter("status");

// Verify all filters for the field are removed
Assert.assertEquals(0, query.getFilterValues().size());
Assert.assertNull(query.getFilterExpression());
}

@Test
public void testRemoveFilterParameterNonExistentField() {
// Create query with a filter using new API
QueryParameters query = QueryParameters.query("filter=username:eq:test").build();

Assert.assertEquals(1, query.getFilterValues().size());

// Try to remove a non-existent filter
query.removeFilterParameter("nonexistent");

// Verify original filter remains
Assert.assertEquals(1, query.getFilterValues().size());
Assert.assertEquals("username", query.getFilterValues().get(0).getField());
}

@Test
public void testRemoveFilterParameterNullField() {
// Create query with a filter using new API
QueryParameters query = QueryParameters.query("filter=username:eq:test").build();

Assert.assertEquals(1, query.getFilterValues().size());

// Try to remove with null field (should not throw exception)
query.removeFilterParameter(null);

// Verify original filter remains
Assert.assertEquals(1, query.getFilterValues().size());
}

@Test
public void testRemoveFilterParameterEmptyFilters() {
// Create query with no filters
QueryParameters query = new QueryParameters();

// Try to remove a filter (should not throw exception)
query.removeFilterParameter("username");

// Verify still no filters
Assert.assertEquals(0, query.getFilterValues().size());
}

@Test
public void testRemoveFilterParameterFromComplexExpression() {
// Create query with complex nested expression: (username AND email) OR age
QueryParameters query = QueryParameters
.query("filter=(username:eq:test AND email:like:@example.com) OR age:gt:18")
.build();

Assert.assertEquals(3, query.getFilterValues().size());

// Remove username - should leave "email OR age"
query.removeFilterParameter("username");

List<QueryFilter> remainingFilters = query.getFilterValues();
Assert.assertEquals(2, remainingFilters.size());
Assert.assertTrue(remainingFilters.stream().anyMatch(f -> "email".equals(f.getField())));
Assert.assertTrue(remainingFilters.stream().anyMatch(f -> "age".equals(f.getField())));
Assert.assertFalse(remainingFilters.stream().anyMatch(f -> "username".equals(f.getField())));
}

@Test
public void testRemoveFilterParameterCollapseTree() {
// Create query: username AND email
QueryParameters query = QueryParameters
.query("filter=username:eq:test AND email:like:@example.com")
.build();

Assert.assertEquals(2, query.getFilterValues().size());

// Remove username - should collapse to just email (no AND needed)
query.removeFilterParameter("username");

Assert.assertEquals(1, query.getFilterValues().size());
Assert.assertEquals("email", query.getFilterValues().get(0).getField());

// The tree should be collapsed to a single leaf node
Assert.assertNotNull(query.getFilterExpression());
Assert.assertTrue(query.getFilterExpression().isLeaf());
}

@Test
public void testGetFilterValuesWithFilters() {
// Create query with multiple filters using AND
QueryParameters query = QueryParameters
.query("filter=username:eq:test AND email:like:test@example.com")
.build();

List<QueryFilter> filters = query.getFilterValues();

Assert.assertNotNull(filters);
Assert.assertEquals(2, filters.size());
Assert.assertTrue(filters.stream().anyMatch(f -> "username".equals(f.getField())));
Assert.assertTrue(filters.stream().anyMatch(f -> "email".equals(f.getField())));
}

@Test
public void testGetFilterValuesEmptyExpression() {
// Create empty query
QueryParameters query = new QueryParameters();

List<QueryFilter> filters = query.getFilterValues();

// Should return empty list, not null
Assert.assertNotNull(filters);
Assert.assertEquals(0, filters.size());
}

@Test
public void testGetFilterValuesNullExpression() {
// Create query and explicitly set null expression
QueryParameters query = new QueryParameters();
query.setFilterExpression(null);

List<QueryFilter> filters = query.getFilterValues();

// Should return empty list, not null
Assert.assertNotNull(filters);
Assert.assertEquals(0, filters.size());
}

@Test
public void testGetFilterValuesComplexExpression() {
// Create query with complex expression (AND/OR)
QueryParameters query = QueryParameters
.query("filter=(username:eq:test AND email:like:test@) OR age:gt:18")
.build();

List<QueryFilter> filters = query.getFilterValues();

Assert.assertNotNull(filters);
Assert.assertEquals(3, filters.size());
Assert.assertTrue(filters.stream().anyMatch(f -> "username".equals(f.getField())));
Assert.assertTrue(filters.stream().anyMatch(f -> "email".equals(f.getField())));
Assert.assertTrue(filters.stream().anyMatch(f -> "age".equals(f.getField())));
}

@Test
public void testGetFilterValuesAfterRemove() {
// Create query with filters using new API
QueryParameters query = QueryParameters
.query("filter=username:eq:test AND email:like:test@example.com")
.build();

Assert.assertEquals(2, query.getFilterValues().size());

// Remove a filter
query.removeFilterParameter("username");

// Get filters again
List<QueryFilter> filters = query.getFilterValues();

Assert.assertNotNull(filters);
Assert.assertEquals(1, filters.size());
Assert.assertEquals("email", filters.get(0).getField());
}
}
Loading