diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/AbstractHTMLWidgetRenderer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/AbstractHTMLWidgetRenderer.java
index 3705968c..26481e78 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/AbstractHTMLWidgetRenderer.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/AbstractHTMLWidgetRenderer.java
@@ -41,6 +41,7 @@ import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
+import com.kingsrook.qqq.backend.core.utils.QQueryFilterDeduper;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
@@ -176,11 +177,13 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
{
return (totalString);
}
+ filter = QQueryFilterDeduper.dedupeFilter(filter);
return ("" + totalString + "");
}
+
/*******************************************************************************
**
*******************************************************************************/
@@ -192,6 +195,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
return;
}
+ filter = QQueryFilterDeduper.dedupeFilter(filter);
urls.add(tablePath + "?filter=" + JsonUtils.toJson(filter));
}
@@ -208,6 +212,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
return (null);
}
+ filter = QQueryFilterDeduper.dedupeFilter(filter);
return (tablePath + "?filter=" + JsonUtils.toJson(filter));
}
@@ -224,6 +229,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
return (null);
}
+ filter = QQueryFilterDeduper.dedupeFilter(filter);
return (tablePath + "?filter=" + URLEncoder.encode(JsonUtils.toJson(filter), Charset.defaultCharset()));
}
@@ -326,6 +332,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
}
String tablePath = QContext.getQInstance().getTablePath(tableName);
+ filter = QQueryFilterDeduper.dedupeFilter(filter);
return (tablePath + "/" + processName + "?recordsParam=filterJSON&filterJSON=" + URLEncoder.encode(JsonUtils.toJson(filter), StandardCharsets.UTF_8));
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/QQueryFilterDeduper.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/QQueryFilterDeduper.java
new file mode 100644
index 00000000..b6d6e0d0
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/QQueryFilterDeduper.java
@@ -0,0 +1,363 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.utils;
+
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import com.kingsrook.qqq.backend.core.logging.QLogger;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
+import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
+import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.EQUALS;
+import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.IN;
+import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.NOT_EQUALS;
+import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.NOT_IN;
+
+
+/*******************************************************************************
+ ** Class to help deduplicate redundant criteria in filters.
+ **
+ ** Original use-case is for making more clean url links out of filters.
+ **
+ ** Does not (at this time) look into sub-filters at all, or support any "OR"
+ ** filters other than the most basic (a=1 OR a=1).
+ **
+ ** Also, other than for completely redundant criteria (e.g., a>1 and a>1) only
+ * works on a limited subset of criteria operators (EQUALS, NOT_EQUALS, IN, and NOT_IN)
+ *******************************************************************************/
+public class QQueryFilterDeduper
+{
+ private static final QLogger LOG = QLogger.getLogger(QQueryFilterDeduper.class);
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static QQueryFilter dedupeFilter(QQueryFilter filter)
+ {
+ if(filter == null)
+ {
+ return (null);
+ }
+
+ try
+ {
+ /////////////////////////////////////////////////////////////////
+ // track (just for logging) if we failed or if we did any good //
+ /////////////////////////////////////////////////////////////////
+ List log = new ArrayList<>();
+ boolean fail = false;
+ boolean didAnyGood = false;
+
+ //////////////////////////////////////////////////////////////////////////////////////////
+ // always create a clone to be returned. this is especially useful because, //
+ // the clone's lists will be ArrayLists, which are mutable - since some of the deduping //
+ // involves manipulating value lists. //
+ //////////////////////////////////////////////////////////////////////////////////////////
+ QQueryFilter rs = filter.clone();
+
+ ////////////////////////////////////////////////////////////////////////////////////
+ // general strategy is: //
+ // iterate over criteria, possibly removing the one the iterator is pointing at, //
+ // if we are able to somehow merge it into other criteria we've already seen. //
+ // the others-we've-seen will be tracked in the criteriaByFieldName listing hash. //
+ ////////////////////////////////////////////////////////////////////////////////////
+ ListingHash criteriaByFieldName = new ListingHash<>();
+ Iterator iterator = rs.getCriteria().iterator();
+ while(iterator.hasNext())
+ {
+ QFilterCriteria criteria = iterator.next();
+
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // first thing to check is, have we seen any other criteria for this field - if so - try to do some de-duping. //
+ // note that, any time we do a remove, we'll need to do a continue - to avoid adding the now-removed criteria //
+ // to the listing hash //
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ if(criteriaByFieldName.containsKey(criteria.getFieldName()))
+ {
+ List others = criteriaByFieldName.get(criteria.getFieldName());
+ QFilterCriteria other = others.get(0);
+
+ if(others.size() == 1 && other.equals(criteria))
+ {
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // if we've only see 1 other criteria for this field so far, and this one is an exact match, then remove this one. //
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ log.add(String.format("Remove duplicate criteria [%s]", criteria));
+ iterator.remove();
+ didAnyGood = true;
+ continue;
+ }
+ else
+ {
+ /////////////////////////////////////////////////////////////////////////////////////////
+ // else - if there's still just 1 other, and it's an AND query - then apply some basic //
+ // logic-merging operations, based on the pair of criteria operators //
+ /////////////////////////////////////////////////////////////////////////////////////////
+ if(others.size() == 1 && QQueryFilter.BooleanOperator.AND.equals(filter.getBooleanOperator()))
+ {
+ if((NOT_EQUALS.equals(other.getOperator()) || NOT_IN.equals(other.getOperator())) && EQUALS.equals(criteria.getOperator()))
+ {
+ ///////////////////////////////////////////////////////////////////////////
+ // if we previously saw a not-equals or not-in, and now we see an equals //
+ // and the value from the EQUALS isn't in the not-in list //
+ // then replace the not-equals with the equals //
+ // then just discard this equals //
+ ///////////////////////////////////////////////////////////////////////////
+ if(other.getValues().contains(criteria.getValues().get(0)))
+ {
+ log.add("Contradicting NOT_EQUALS/NOT_IN and EQUALS");
+ fail = true;
+ }
+ else
+ {
+ other.setOperator(criteria.getOperator());
+ other.setValues(criteria.getValues());
+ iterator.remove();
+ didAnyGood = true;
+ log.add("Replace a not-equals or not-in superseded by an equals");
+ continue;
+ }
+ }
+ else if(EQUALS.equals(other.getOperator()) && (NOT_EQUALS.equals(criteria.getOperator()) || NOT_IN.equals(criteria.getOperator())))
+ {
+ /////////////////////////////////////////////////////////////////////////////
+ // if we previously saw an equals, and now we see a not-equals or a not-in //
+ // and the value from the EQUALS isn't in the not-in list //
+ // then just discard this not-equals //
+ /////////////////////////////////////////////////////////////////////////////
+ if(criteria.getValues().contains(other.getValues().get(0)))
+ {
+ log.add("Contradicting NOT_EQUALS/NOT_IN and EQUALS");
+ fail = true;
+ }
+ else
+ {
+ iterator.remove();
+ didAnyGood = true;
+ log.add("Remove a redundant not-equals");
+ continue;
+ }
+ }
+ else if(NOT_EQUALS.equals(other.getOperator()) && IN.equals(criteria.getOperator()))
+ {
+ /////////////////////////////////////////////////////////////////////////////////////////////////////
+ // if we previously saw a not-equals, and now we see an IN //
+ // then replace the not-equals with the IN (making sure the not-equals value isn't in the in-list) //
+ // then just discard this equals //
+ /////////////////////////////////////////////////////////////////////////////////////////////////////
+ Serializable notEqualsValue = other.getValues().get(0);
+ List inValues = new ArrayList<>(criteria.getValues());
+ inValues.remove(notEqualsValue);
+ if(inValues.isEmpty())
+ {
+ ///////////////////////////////////////////////////////////////////////////////////
+ // if the only in-value was the not-equal value, then... i don't know, don't try //
+ ///////////////////////////////////////////////////////////////////////////////////
+ log.add("Contradicting IN and NOT_EQUAL");
+ fail = true;
+ }
+ else
+ {
+ //////////////////////////////////////////////////////////////////
+ // else, we can proceed by replacing the not-equals with the in //
+ //////////////////////////////////////////////////////////////////
+ other.setOperator(criteria.getOperator());
+ other.setValues(criteria.getValues());
+ iterator.remove();
+ didAnyGood = true;
+ log.add("Replace superseded not-equals (removing its value from in-list)");
+ continue;
+ }
+ }
+ else if(IN.equals(other.getOperator()) && NOT_EQUALS.equals(criteria.getOperator()))
+ {
+ //////////////////////////////////////////////////////////////////
+ // if we previously saw an in, and now we see a not-equals //
+ // discard the not-equals (removing its value from the in-list) //
+ // then just discard this not-equals //
+ //////////////////////////////////////////////////////////////////
+ Serializable notEqualsValue = criteria.getValues().get(0);
+ List inValues = new ArrayList<>(other.getValues());
+ inValues.remove(notEqualsValue);
+ if(inValues.isEmpty())
+ {
+ ///////////////////////////////////////////////////////////////////////////////////
+ // if the only in-value was the not-equal value, then... i don't know, don't try //
+ ///////////////////////////////////////////////////////////////////////////////////
+ log.add("Contradicting IN and NOT_EQUAL");
+ fail = true;
+ }
+ else
+ {
+ //////////////////////////////////////////////////////////////////
+ // else, we can proceed by replacing the not-equals with the in //
+ //////////////////////////////////////////////////////////////////
+ iterator.remove();
+ didAnyGood = true;
+ log.add("Remove redundant not-equals (removing its value from in-list)");
+ continue;
+ }
+ }
+ else if(NOT_EQUALS.equals(other.getOperator()) && NOT_IN.equals(criteria.getOperator()))
+ {
+ /////////////////////////////////////////////////////////////////////////////////////////
+ // if we previously saw a not-equals, and now we see a not-in //
+ // we can change the not-equals to the not-in, and make sure it's value is in the list //
+ // then just discard this not-in //
+ /////////////////////////////////////////////////////////////////////////////////////////
+ Serializable originalNotEqualsValue = other.getValues().get(0);
+ other.setOperator(criteria.getOperator());
+ other.setValues(criteria.getValues());
+ if(!other.getValues().contains(originalNotEqualsValue))
+ {
+ other.getValues().add(originalNotEqualsValue);
+ }
+ iterator.remove();
+ didAnyGood = true;
+ log.add("Replace superseded not-equals with not-in");
+ continue;
+ }
+ else if(NOT_IN.equals(other.getOperator()) && NOT_EQUALS.equals(criteria.getOperator()))
+ {
+ ////////////////////////////////////////////////////////////////////////////////////////
+ // if we previously saw a not-in, and now we see a not-equals //
+ // we can discard this not-equals, and just make sure its value is in the not-in list //
+ ////////////////////////////////////////////////////////////////////////////////////////
+ Serializable originalNotEqualsValue = criteria.getValues().get(0);
+ if(!other.getValues().contains(originalNotEqualsValue))
+ {
+ other.getValues().add(originalNotEqualsValue);
+ }
+ iterator.remove();
+ didAnyGood = true;
+ log.add("Remove not-equals, absorbing into not-in");
+ continue;
+ }
+ else if(NOT_IN.equals(other.getOperator()) && NOT_IN.equals(criteria.getOperator()))
+ {
+ ////////////////////////////////////////////////////////////////
+ // for multiple not-ins, just merge their values (as a union) //
+ ////////////////////////////////////////////////////////////////
+ for(Serializable value : criteria.getValues())
+ {
+ if(!other.getValues().contains(value))
+ {
+ other.getValues().add(value);
+ }
+ }
+ iterator.remove();
+ didAnyGood = true;
+ log.add("Merging not-ins");
+ continue;
+ }
+ else if(IN.equals(other.getOperator()) && IN.equals(criteria.getOperator()))
+ {
+ ////////////////////////////////////////////////////////////////////////
+ // for multiple not-ins, just merge their values (as an intersection) //
+ ////////////////////////////////////////////////////////////////////////
+ Set otherValues = new HashSet<>(other.getValues());
+ Set criteriaValues = new HashSet<>(criteria.getValues());
+ otherValues.retainAll(criteriaValues);
+ if(otherValues.isEmpty())
+ {
+ log.add("Contradicting IN lists (no values)");
+ fail = true;
+ }
+ else
+ {
+ other.setValues(new ArrayList<>(otherValues));
+ iterator.remove();
+ didAnyGood = true;
+ log.add("Merging not-ins");
+ continue;
+ }
+ }
+ else if(NOT_EQUALS.equals(other.getOperator()) && NOT_EQUALS.equals(criteria.getOperator()))
+ {
+ /////////////////////////////////////////////////////////////////////////////////////
+ // if we have 2 not-equals, we can merge them in a not-in //
+ // we can assume their values are different, else they'd have been equals up above //
+ /////////////////////////////////////////////////////////////////////////////////////
+ other.setOperator(NOT_IN);
+ other.setValues(new ArrayList<>(List.of(other.getValues().get(0), criteria.getValues().get(0))));
+ iterator.remove();
+ didAnyGood = true;
+ log.add("Merge two not-equals as not-in");
+ continue;
+ }
+ else
+ {
+ log.add("Fail because unhandled operator pair");
+ fail = true;
+ }
+ }
+ else
+ {
+ log.add("Fail because > 1 other or operator: OR");
+ fail = true;
+ }
+ }
+ }
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // if we reach here (e.g., no continue), then assuming we didn't remove the criteria, add it to the listing hash. //
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ criteriaByFieldName.add(criteria.getFieldName(), criteria);
+ }
+
+ ///////////////////////////
+ // log based on booleans //
+ ///////////////////////////
+ if(fail && didAnyGood)
+ {
+ LOG.info("Partially unsuccessful dedupe of filter", logPair("original", filter), logPair("deduped", rs), logPair("log", log));
+ }
+ else if(fail)
+ {
+ LOG.info("Unsuccessful dedupe of filter", logPair("filter", filter), logPair("log", log));
+ }
+ else if(didAnyGood)
+ {
+ LOG.debug("Successful dedupe of filter", logPair("original", filter), logPair("deduped", rs), logPair("log", log));
+ }
+ else
+ {
+ LOG.debug("No duplicates in filter, so nothing to dedupe", logPair("original", filter));
+ }
+
+ return rs;
+ }
+ catch(Exception e)
+ {
+ LOG.warn("Error de-duping filter", e, logPair("filter", filter));
+ return (filter.clone());
+ }
+ }
+
+}
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/AbstractHTMLWidgetRendererTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/AbstractHTMLWidgetRendererTest.java
new file mode 100644
index 00000000..22d23494
--- /dev/null
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/AbstractHTMLWidgetRendererTest.java
@@ -0,0 +1,58 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.dashboard;
+
+
+import com.kingsrook.qqq.backend.core.BaseTest;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
+import com.kingsrook.qqq.backend.core.utils.TestUtils;
+import org.junit.jupiter.api.Test;
+import static org.assertj.core.api.Assertions.assertThat;
+
+
+/*******************************************************************************
+ ** Unit test for AbstractHTMLWidgetRenderer
+ *******************************************************************************/
+class AbstractHTMLWidgetRendererTest extends BaseTest
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void test() throws QException
+ {
+ String link = AbstractHTMLWidgetRenderer.getCountLink(null, TestUtils.TABLE_NAME_PERSON, new QQueryFilter()
+ .withCriteria(new QFilterCriteria("a", QCriteriaOperator.EQUALS, 1))
+ .withCriteria(new QFilterCriteria("a", QCriteriaOperator.EQUALS, 1)), 2
+ );
+
+ ////////////////////////////////////////////////////
+ // assert that filter de-duplication is occurring //
+ ////////////////////////////////////////////////////
+ assertThat(link).doesNotMatch(".*EQUALS.*EQUALS.*");
+ }
+
+}
\ No newline at end of file
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/QQueryFilterDeduperTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/QQueryFilterDeduperTest.java
new file mode 100644
index 00000000..2f596de6
--- /dev/null
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/QQueryFilterDeduperTest.java
@@ -0,0 +1,355 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.utils;
+
+
+import com.kingsrook.qqq.backend.core.BaseTest;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
+import org.junit.jupiter.api.Test;
+import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.EQUALS;
+import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.GREATER_THAN;
+import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.IN;
+import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.NOT_EQUALS;
+import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator.NOT_IN;
+import static com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter.BooleanOperator.OR;
+import static com.kingsrook.qqq.backend.core.utils.QQueryFilterDeduper.dedupeFilter;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+
+/*******************************************************************************
+ ** Unit test for QQueryFilterDeduper
+ *******************************************************************************/
+class QQueryFilterDeduperTest extends BaseTest
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testDegenerateCases()
+ {
+ assertNull(dedupeFilter(null));
+
+ QQueryFilter empty = new QQueryFilter();
+ assertEquals(empty, dedupeFilter(empty));
+ assertNotSame(empty, dedupeFilter(empty)); // method always clones, so, just assert that.
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testSimpleFiltersWithNoChanges()
+ {
+ QQueryFilter oneCriteria = new QQueryFilter()
+ .withCriteria(new QFilterCriteria("a", EQUALS, 1));
+ assertEquals(oneCriteria, dedupeFilter(oneCriteria));
+ assertNotSame(oneCriteria, dedupeFilter(oneCriteria));
+
+ QQueryFilter twoCriteriaDifferentFields = new QQueryFilter()
+ .withCriteria(new QFilterCriteria("a", EQUALS, 1))
+ .withCriteria(new QFilterCriteria("b", GREATER_THAN, 2));
+ assertEquals(twoCriteriaDifferentFields, dedupeFilter(twoCriteriaDifferentFields));
+ assertNotSame(twoCriteriaDifferentFields, dedupeFilter(twoCriteriaDifferentFields));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testOrs()
+ {
+ ///////////////////////////////////////////////////////
+ // we've only written the simplest cases with ORs... //
+ ///////////////////////////////////////////////////////
+ assertEquals(new QQueryFilter().withBooleanOperator(OR).withCriteria(new QFilterCriteria("a", EQUALS, 1)), dedupeFilter(new QQueryFilter()
+ .withBooleanOperator(OR)
+ .withCriteria(new QFilterCriteria("a", EQUALS, 1))
+ .withCriteria(new QFilterCriteria("a", EQUALS, 1))
+ .withCriteria(new QFilterCriteria("a", EQUALS, 1))
+ ));
+
+ //////////////////////////////////////////////////////////////////////
+ // just not built at this time - obviously, could become an IN list //
+ //////////////////////////////////////////////////////////////////////
+ QQueryFilter notSupportedOrTwoEquals = new QQueryFilter()
+ .withBooleanOperator(OR)
+ .withCriteria(new QFilterCriteria("f", EQUALS, 1))
+ .withCriteria(new QFilterCriteria("f", EQUALS, 2));
+ assertEquals(notSupportedOrTwoEquals, dedupeFilter(notSupportedOrTwoEquals));
+
+ ///////////////////////////////////////////////////////////////////////////////////
+ // I think the logic would be, that the EQUALS 1 would be removed (is redundant) //
+ ///////////////////////////////////////////////////////////////////////////////////
+ QQueryFilter notSupportedOrEqualsNotEquals = new QQueryFilter()
+ .withBooleanOperator(OR)
+ .withCriteria(new QFilterCriteria("f", EQUALS, 1))
+ .withCriteria(new QFilterCriteria("f", NOT_EQUALS, 2));
+ assertEquals(notSupportedOrEqualsNotEquals, dedupeFilter(notSupportedOrEqualsNotEquals));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testMoreOperators()
+ {
+ //////////////////////////////////////////////////////////////////////
+ // only simplest case (of criteria being .equals()) is supported... //
+ //////////////////////////////////////////////////////////////////////
+ assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("a", GREATER_THAN, 1)), dedupeFilter(new QQueryFilter()
+ .withCriteria(new QFilterCriteria("a", GREATER_THAN, 1))
+ .withCriteria(new QFilterCriteria("a", GREATER_THAN, 1))
+ ));
+
+ ///////////////////////////////////////////////////////////////////////////////////
+ // in theory, we could do more, but we just haven't yet (e.g, this could be > 5) //
+ ///////////////////////////////////////////////////////////////////////////////////
+ QQueryFilter tooComplex = new QQueryFilter()
+ .withCriteria(new QFilterCriteria("f", GREATER_THAN, 1))
+ .withCriteria(new QFilterCriteria("f", GREATER_THAN, 5));
+ assertEquals(tooComplex, dedupeFilter(tooComplex));
+
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testAllEquals()
+ {
+ assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("a", EQUALS, 1)), dedupeFilter(new QQueryFilter()
+ .withCriteria(new QFilterCriteria("a", EQUALS, 1))
+ .withCriteria(new QFilterCriteria("a", EQUALS, 1))
+ ));
+
+ assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("a", EQUALS, 1)), dedupeFilter(new QQueryFilter()
+ .withCriteria(new QFilterCriteria("a", EQUALS, 1))
+ .withCriteria(new QFilterCriteria("a", EQUALS, 1))
+ .withCriteria(new QFilterCriteria("a", EQUALS, 1))
+ ));
+
+ assertEquals(new QQueryFilter()
+ .withCriteria(new QFilterCriteria("a", EQUALS, 1))
+ .withCriteria(new QFilterCriteria("b", EQUALS, 2))
+ .withCriteria(new QFilterCriteria("c", EQUALS, 3)),
+ dedupeFilter(new QQueryFilter()
+ .withCriteria(new QFilterCriteria("a", EQUALS, 1))
+ .withCriteria(new QFilterCriteria("b", EQUALS, 2))
+ .withCriteria(new QFilterCriteria("a", EQUALS, 1))
+ .withCriteria(new QFilterCriteria("b", EQUALS, 2))
+ .withCriteria(new QFilterCriteria("b", EQUALS, 2))
+ .withCriteria(new QFilterCriteria("a", EQUALS, 1))
+ .withCriteria(new QFilterCriteria("c", EQUALS, 3))
+ .withCriteria(new QFilterCriteria("c", EQUALS, 3))
+ .withCriteria(new QFilterCriteria("c", EQUALS, 3))
+ ));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testEqualsAndNotEqualsAndNotIn()
+ {
+ assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", EQUALS, 1)), dedupeFilter(new QQueryFilter()
+ .withCriteria(new QFilterCriteria("f", EQUALS, 1))
+ .withCriteria(new QFilterCriteria("f", NOT_EQUALS, 2))
+ ));
+
+ assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", EQUALS, 1)), dedupeFilter(new QQueryFilter()
+ .withCriteria(new QFilterCriteria("f", EQUALS, 1))
+ .withCriteria(new QFilterCriteria("f", NOT_EQUALS, 2))
+ .withCriteria(new QFilterCriteria("f", NOT_EQUALS, 3))
+ ));
+
+ assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", EQUALS, 1)), dedupeFilter(new QQueryFilter()
+ .withCriteria(new QFilterCriteria("f", NOT_EQUALS, 2))
+ .withCriteria(new QFilterCriteria("f", EQUALS, 1))
+ .withCriteria(new QFilterCriteria("f", NOT_EQUALS, 3))
+ ));
+
+ assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", EQUALS, 1)), dedupeFilter(new QQueryFilter()
+ .withCriteria(new QFilterCriteria("f", NOT_EQUALS, 2))
+ .withCriteria(new QFilterCriteria("f", NOT_EQUALS, 3))
+ .withCriteria(new QFilterCriteria("f", EQUALS, 1))
+ ));
+
+ assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", EQUALS, 1)), dedupeFilter(new QQueryFilter()
+ .withCriteria(new QFilterCriteria("f", EQUALS, 1))
+ .withCriteria(new QFilterCriteria("f", NOT_IN, 2, 3))
+ ));
+
+ assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", EQUALS, 1)), dedupeFilter(new QQueryFilter()
+ .withCriteria(new QFilterCriteria("f", EQUALS, 1))
+ .withCriteria(new QFilterCriteria("f", NOT_IN, 2, 3))
+ .withCriteria(new QFilterCriteria("f", NOT_EQUALS, 4))
+ ));
+
+ assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", EQUALS, 1)), dedupeFilter(new QQueryFilter()
+ .withCriteria(new QFilterCriteria("f", NOT_IN, 2, 3))
+ .withCriteria(new QFilterCriteria("f", NOT_EQUALS, 4))
+ .withCriteria(new QFilterCriteria("f", EQUALS, 1))
+ ));
+
+ assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", EQUALS, 1)), dedupeFilter(new QQueryFilter()
+ .withCriteria(new QFilterCriteria("f", NOT_IN, 2, 3))
+ .withCriteria(new QFilterCriteria("f", EQUALS, 1))
+ .withCriteria(new QFilterCriteria("f", NOT_EQUALS, 4))
+ ));
+
+ ////////////////////////////////////////////////////////////
+ // this is a contradiction, so we choose not to dedupe it //
+ ////////////////////////////////////////////////////////////
+ QQueryFilter contradiction1 = new QQueryFilter()
+ .withCriteria(new QFilterCriteria("f", EQUALS, 1))
+ .withCriteria(new QFilterCriteria("f", NOT_EQUALS, 1));
+ assertEquals(contradiction1, dedupeFilter(contradiction1));
+
+ QQueryFilter contradiction2 = new QQueryFilter()
+ .withCriteria(new QFilterCriteria("f", EQUALS, 1))
+ .withCriteria(new QFilterCriteria("f", NOT_IN, 0, 1));
+ assertEquals(contradiction2, dedupeFilter(contradiction2));
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // this case can collapse the two not-equals, but then fails to merge the equals with them, because they are a contradiction! //
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ assertEquals(new QQueryFilter()
+ .withCriteria(new QFilterCriteria("f", NOT_IN, 2, 3))
+ .withCriteria(new QFilterCriteria("f", EQUALS, 2)),
+ dedupeFilter(new QQueryFilter()
+ .withCriteria(new QFilterCriteria("f", NOT_EQUALS, 2))
+ .withCriteria(new QFilterCriteria("f", NOT_EQUALS, 3))
+ .withCriteria(new QFilterCriteria("f", EQUALS, 2))
+ ));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testNotEqualsAndNotIn()
+ {
+ assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", NOT_IN, 1, 2, 3)), dedupeFilter(new QQueryFilter()
+ .withCriteria(new QFilterCriteria("f", NOT_EQUALS, 1))
+ .withCriteria(new QFilterCriteria("f", NOT_EQUALS, 2))
+ .withCriteria(new QFilterCriteria("f", NOT_EQUALS, 3))
+ ));
+
+ //////////////////////////////////////////////////////////////////////////////////////////
+ // ideally, maybe, this would have the values ordered 1,2,3, but, is equivalent enough //
+ //////////////////////////////////////////////////////////////////////////////////////////
+ assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", NOT_IN, 2, 3, 1)), dedupeFilter(new QQueryFilter()
+ .withCriteria(new QFilterCriteria("f", NOT_EQUALS, 1))
+ .withCriteria(new QFilterCriteria("f", NOT_IN, 2, 3))
+ ));
+
+ assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", NOT_IN, 2, 3, 1)), dedupeFilter(new QQueryFilter()
+ .withCriteria(new QFilterCriteria("f", NOT_IN, 2, 3))
+ .withCriteria(new QFilterCriteria("f", NOT_EQUALS, 1))
+ ));
+
+ assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", NOT_IN, 1, 2, 3)), dedupeFilter(new QQueryFilter()
+ .withCriteria(new QFilterCriteria("f", NOT_IN, 1, 2))
+ .withCriteria(new QFilterCriteria("f", NOT_IN, 2, 3))
+ ));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testInAndNotEquals()
+ {
+ assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", IN, 2, 3)), dedupeFilter(new QQueryFilter()
+ .withCriteria(new QFilterCriteria("f", NOT_EQUALS, 1))
+ .withCriteria(new QFilterCriteria("f", IN, 2, 3))
+ ));
+
+ assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", IN, 2, 3)), dedupeFilter(new QQueryFilter()
+ .withCriteria(new QFilterCriteria("f", IN, 2, 3))
+ .withCriteria(new QFilterCriteria("f", NOT_EQUALS, 1))
+ ));
+
+ QQueryFilter contradiction1 = new QQueryFilter()
+ .withCriteria(new QFilterCriteria("f", NOT_EQUALS, 1))
+ .withCriteria(new QFilterCriteria("f", IN, 1));
+ assertEquals(contradiction1, dedupeFilter(contradiction1));
+
+ QQueryFilter contradiction2 = new QQueryFilter()
+ .withCriteria(new QFilterCriteria("f", IN, 1))
+ .withCriteria(new QFilterCriteria("f", NOT_EQUALS, 1));
+ assertEquals(contradiction2, dedupeFilter(contradiction2));
+
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testMultipleInLists()
+ {
+ assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", IN, 2)), dedupeFilter(new QQueryFilter()
+ .withCriteria(new QFilterCriteria("f", IN, 1, 2))
+ .withCriteria(new QFilterCriteria("f", IN, 2, 3))
+ ));
+
+ assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", IN, 3, 4)), dedupeFilter(new QQueryFilter()
+ .withCriteria(new QFilterCriteria("f", IN, 1, 2, 3, 4))
+ .withCriteria(new QFilterCriteria("f", IN, 3, 4, 5, 6))
+ ));
+
+ assertEquals(new QQueryFilter().withCriteria(new QFilterCriteria("f", IN, 3)), dedupeFilter(new QQueryFilter()
+ .withCriteria(new QFilterCriteria("f", IN, 1, 2, 3, 4))
+ .withCriteria(new QFilterCriteria("f", IN, 3, 4, 5, 6))
+ .withCriteria(new QFilterCriteria("f", IN, 1, 3, 5, 7))
+ ));
+
+ ///////////////////////////////////////////////////////////////////
+ // contradicting in-lists - we give up and refuse to simplify it //
+ ///////////////////////////////////////////////////////////////////
+ QQueryFilter contradiction = new QQueryFilter()
+ .withCriteria(new QFilterCriteria("f", IN, 1, 2))
+ .withCriteria(new QFilterCriteria("f", IN, 3, 4));
+ assertEquals(contradiction, dedupeFilter(contradiction));
+ }
+
+}
\ No newline at end of file