You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@directory.apache.org by bd...@apache.org on 2022/07/28 22:34:54 UTC

[directory-scimple] 01/02: Update client Filter tests

This is an automated email from the ASF dual-hosted git repository.

bdemers pushed a commit to branch move-client-filter-logic
in repository https://gitbox.apache.org/repos/asf/directory-scimple.git

commit 80d50c5db821ec13ab5cfa2126417e79a15ce763
Author: Brian Demers <bd...@apache.org>
AuthorDate: Thu Jul 28 18:10:06 2022 -0400

    Update client Filter tests
    
    * Moved FilterClient to FilterBuilder
    * Added validation logic in tests
---
 scim-client/pom.xml                                |   5 +
 .../directory/scim/client/filter/FilterClient.java | 670 ---------------------
 .../scim/client/rest/ComparisonBuilder.java        | 363 +++++++++++
 .../scim/client/rest/ComplexLogicalBuilder.java    |  65 ++
 .../directory/scim/client/rest/FilterBuilder.java  | 102 ++++
 .../scim/client/rest/SimpleLogicalBuilder.java     | 121 ++++
 .../client/filter/FilterBuilderEqualsTest.java     | 118 ++++
 .../client/filter/FilterBuilderGreaterTest.java    | 165 ++---
 .../client/filter/FilterBuilderLessThanTest.java   | 152 ++---
 .../client/filter/FilterBuilderNotEqualsTest.java  | 119 ++++
 ...ringTests.java => FilterBuilderStringTest.java} |  41 +-
 .../scim/client/filter/FilterBuilderTest.java      | 279 ++++-----
 .../client/filter/FilterBuilderTestEquals.java     | 111 ----
 .../client/filter/FilterBuilderTestNotEquals.java  | 111 ----
 .../filter/ExpressionBuildingListener.java         |  11 +-
 .../scim/spec/protocol/search/Filter.java          |  41 +-
 16 files changed, 1257 insertions(+), 1217 deletions(-)

diff --git a/scim-client/pom.xml b/scim-client/pom.xml
index 384307d..4f16089 100644
--- a/scim-client/pom.xml
+++ b/scim-client/pom.xml
@@ -49,6 +49,11 @@
       <artifactId>junit-jupiter</artifactId>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>org.assertj</groupId>
+      <artifactId>assertj-core</artifactId>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
   
 </project>
diff --git a/scim-client/src/main/java/org/apache/directory/scim/client/filter/FilterClient.java b/scim-client/src/main/java/org/apache/directory/scim/client/filter/FilterClient.java
deleted file mode 100644
index 693b838..0000000
--- a/scim-client/src/main/java/org/apache/directory/scim/client/filter/FilterClient.java
+++ /dev/null
@@ -1,670 +0,0 @@
-/*
-* Licensed to the Apache Software Foundation (ASF) under one
-* or more contributor license agreements.  See the NOTICE file
-* distributed with this work for additional information
-* regarding copyright ownership.  The ASF licenses this file
-* to you under the Apache License, Version 2.0 (the
-* "License"); you may not use this file except in compliance
-* with the License.  You may obtain a copy of the License at
- 
-* http://www.apache.org/licenses/LICENSE-2.0
-
-* Unless required by applicable law or agreed to in writing,
-* software distributed under the License is distributed on an
-* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-* KIND, either express or implied.  See the License for the
-* specific language governing permissions and limitations
-* under the License.
-*/
-
-package org.apache.directory.scim.client.filter;
-
-import java.io.UnsupportedEncodingException;
-import java.net.URLEncoder;
-import java.time.LocalDate;
-import java.time.LocalDateTime;
-import java.util.Date;
-
-import org.apache.directory.scim.spec.protocol.attribute.AttributeReference;
-import org.apache.directory.scim.spec.protocol.filter.AttributeComparisonExpression;
-import org.apache.directory.scim.spec.protocol.filter.CompareOperator;
-import org.apache.directory.scim.spec.protocol.filter.FilterExpression;
-import org.apache.directory.scim.spec.protocol.filter.FilterParseException;
-import org.apache.directory.scim.spec.protocol.filter.GroupExpression;
-import org.apache.directory.scim.spec.protocol.filter.LogicalExpression;
-import org.apache.directory.scim.spec.protocol.filter.LogicalOperator;
-import org.apache.directory.scim.spec.protocol.filter.ValuePathExpression;
-import org.apache.directory.scim.spec.protocol.search.Filter;
-import lombok.extern.slf4j.Slf4j;
-
-@Slf4j
-public class FilterClient {
-
-  private FilterClient() {
-    
-  }
-  
-  public abstract static class SimpleLogicalBuilder extends Builder {
-    SimpleLogicalBuilder() {
-    }
-
-    @Override
-    public Builder and() {
-      if (filterExpression == null) {
-        throw new IllegalStateException();
-      }
-
-      LogicalExpression logicalExpression = new LogicalExpression();
-      logicalExpression.setLeft(filterExpression);
-      logicalExpression.setOperator(LogicalOperator.AND);
-      filterExpression = logicalExpression;
-
-      return this;
-    }
-
-    @Override
-    public Builder or() {
-      if (filterExpression == null) {
-        throw new IllegalStateException();
-      }
-
-      LogicalExpression logicalExpression = new LogicalExpression();
-      logicalExpression.setLeft(filterExpression);
-      logicalExpression.setOperator(LogicalOperator.OR);
-      filterExpression = logicalExpression;
-
-      return this;
-    }
-  }
-
-  public abstract static class ComplexLogicalBuilder extends SimpleLogicalBuilder {
-
-    @Override
-    public Builder or(FilterExpression fe1) {
-      if (filterExpression instanceof AttributeComparisonExpression) {
-        LogicalExpression logicalExpression = new LogicalExpression();
-        logicalExpression.setLeft(filterExpression);
-        logicalExpression.setRight(fe1);
-        logicalExpression.setOperator(LogicalOperator.OR);
-        filterExpression = logicalExpression;
-        return this;
-      }
-      
-      LogicalExpression logicalExpression = new LogicalExpression();
-      logicalExpression.setLeft(fe1);
-      logicalExpression.setOperator(LogicalOperator.OR);
-
-      return handleLogicalExpression(logicalExpression, LogicalOperator.OR);
-    }
-    
-    @Override
-    public Builder or(FilterExpression fe1, FilterExpression fe2) {
-      LogicalExpression logicalExpression = new LogicalExpression();
-      logicalExpression.setLeft(fe1);
-      logicalExpression.setRight(fe2);
-      logicalExpression.setOperator(LogicalOperator.OR);
-
-      return handleLogicalExpression(logicalExpression, LogicalOperator.OR);
-    }
-
-    @Override
-    public Builder and(FilterExpression fe1, FilterExpression fe2) {
-      LogicalExpression logicalExpression = new LogicalExpression();
-      logicalExpression.setLeft(fe1);
-      logicalExpression.setRight(fe2);
-      logicalExpression.setOperator(LogicalOperator.AND);
-
-      return handleLogicalExpression(logicalExpression, LogicalOperator.AND);
-    }
-    
-    @Override
-    public Builder and(FilterExpression fe1) {
-      if (filterExpression instanceof AttributeComparisonExpression) {
-        LogicalExpression logicalExpression = new LogicalExpression();
-        logicalExpression.setLeft(filterExpression);
-        logicalExpression.setRight(fe1);
-        logicalExpression.setOperator(LogicalOperator.AND);
-        filterExpression = logicalExpression;
-        return this;
-      }
-      
-      LogicalExpression logicalExpression = new LogicalExpression();
-      logicalExpression.setLeft(fe1);
-      logicalExpression.setOperator(LogicalOperator.AND);
-
-      return handleLogicalExpression(logicalExpression, LogicalOperator.AND);
-    }
-  }
-
-  // This class is returned from comparison operations to ensure
-  // that the next step is correct
-  public static class ComparisonBuilder extends ComplexLogicalBuilder {
-
-    @Override
-    public Builder equalTo(String key, String value) {
-
-      AttributeReference ar = new AttributeReference(key);
-      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.EQ, value);
-
-      handleComparisonExpression(filterExpression);
-
-      return this;
-    }
-
-    @Override
-    public Builder equalTo(String key, Boolean value) {
-
-      AttributeReference ar = new AttributeReference(key);
-      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.EQ, value);
-
-      handleComparisonExpression(filterExpression);
-
-      return this;
-    }
-
-    @Override
-    public Builder equalTo(String key, Date value) {
-      AttributeReference ar = new AttributeReference(key);
-      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.EQ, value);
-
-      handleComparisonExpression(filterExpression);
-
-      return this;
-    }
-
-    @Override
-    public Builder equalTo(String key, LocalDate value) {
-
-      AttributeReference ar = new AttributeReference(key);
-      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.EQ, value);
-
-      handleComparisonExpression(filterExpression);
-
-      return this;
-    }
-
-    @Override
-    public Builder equalTo(String key, LocalDateTime value) {
-      AttributeReference ar = new AttributeReference(key);
-      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.EQ, value);
-
-      handleComparisonExpression(filterExpression);
-
-      return this;
-    }
-
-    @Override
-    public <T extends Number> Builder equalTo(String key, T value) {
-      AttributeReference ar = new AttributeReference(key);
-      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.EQ, value);
-
-      handleComparisonExpression(filterExpression);
-
-      return this;
-    }
-
-    @Override
-    public Builder equalNull(String key) {
-      AttributeReference ar = new AttributeReference(key);
-      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.EQ, null);
-
-      handleComparisonExpression(filterExpression);
-
-      return this;
-    }
-
-    @Override
-    public Builder notEqual(String key, String value) {
-      AttributeReference ar = new AttributeReference(key);
-      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.NE, value);
-
-      handleComparisonExpression(filterExpression);
-
-      return this;
-    }
-
-    @Override
-    public Builder notEqual(String key, Boolean value) {
-      AttributeReference ar = new AttributeReference(key);
-      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.NE, value);
-
-      handleComparisonExpression(filterExpression);
-
-      return this;
-    }
-
-    @Override
-    public Builder notEqual(String key, Date value) {
-      AttributeReference ar = new AttributeReference(key);
-      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.NE, value);
-
-      handleComparisonExpression(filterExpression);
-
-      return this;
-    }
-
-    @Override
-    public Builder notEqual(String key, LocalDate value) {
-      AttributeReference ar = new AttributeReference(key);
-      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.NE, value);
-
-      handleComparisonExpression(filterExpression);
-
-      return this;
-    }
-
-    @Override
-    public Builder notEqual(String key, LocalDateTime value) {
-      AttributeReference ar = new AttributeReference(key);
-      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.NE, value);
-
-      handleComparisonExpression(filterExpression);
-
-      return this;
-    }
-
-    @Override
-    public <T extends Number> Builder notEqual(String key, T value) {
-      AttributeReference ar = new AttributeReference(key);
-      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.NE, value);
-
-      handleComparisonExpression(filterExpression);
-
-      return this;
-    }
-
-    @Override
-    public Builder notEqualNull(String key) {
-      AttributeReference ar = new AttributeReference(key);
-      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.NE, null);
-
-      handleComparisonExpression(filterExpression);
-
-      return this;
-    }
-
-    @Override
-    public Builder endsWith(String key, String value) {
-      AttributeReference ar = new AttributeReference(key);
-      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.EW, value);
-
-      handleComparisonExpression(filterExpression);
-
-      return this;
-    }
-
-    @Override
-    public Builder startsWith(String key, String value) {
-      AttributeReference ar = new AttributeReference(key);
-      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.SW, value);
-
-      handleComparisonExpression(filterExpression);
-
-      return this;
-    }
-
-    @Override
-    public Builder contains(String key, String value) {
-      AttributeReference ar = new AttributeReference(key);
-      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.CO, value);
-
-      handleComparisonExpression(filterExpression);
-
-      return this;
-    }
-
-    @Override
-    public <T extends Number> Builder greaterThan(String key, T value) {
-      AttributeReference ar = new AttributeReference(key);
-      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.GT, value);
-
-      handleComparisonExpression(filterExpression);
-
-      return this;
-    }
-
-    @Override
-    public Builder greaterThan(String key, Date value) {
-      AttributeReference ar = new AttributeReference(key);
-      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.GT, value);
-
-      handleComparisonExpression(filterExpression);
-
-      return this;
-    }
-
-    @Override
-    public Builder greaterThan(String key, LocalDate value) {
-      AttributeReference ar = new AttributeReference(key);
-      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.GT, value);
-
-      handleComparisonExpression(filterExpression);
-
-      return this;
-    }
-
-    @Override
-    public Builder greaterThan(String key, LocalDateTime value) {
-      AttributeReference ar = new AttributeReference(key);
-      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.GT, value);
-
-      handleComparisonExpression(filterExpression);
-
-      return this;
-    }
-
-    @Override
-    public <T extends Number> Builder greaterThanOrEquals(String key, T value) {
-      AttributeReference ar = new AttributeReference(key);
-      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.GT, value);
-
-      handleComparisonExpression(filterExpression);
-
-      return this;
-    }
-
-    @Override
-    public Builder greaterThanOrEquals(String key, Date value) {
-      AttributeReference ar = new AttributeReference(key);
-      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.GE, value);
-
-      handleComparisonExpression(filterExpression);
-
-      return this;
-    }
-
-    @Override
-    public Builder greaterThanOrEquals(String key, LocalDate value) {
-      AttributeReference ar = new AttributeReference(key);
-      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.GE, value);
-
-      handleComparisonExpression(filterExpression);
-
-      return this;
-    }
-
-    @Override
-    public Builder greaterThanOrEquals(String key, LocalDateTime value) {
-      AttributeReference ar = new AttributeReference(key);
-      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.GE, value);
-
-      handleComparisonExpression(filterExpression);
-
-      return this;
-    }
-
-    @Override
-    public <T extends Number> Builder lessThan(String key, T value) {
-      AttributeReference ar = new AttributeReference(key);
-      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.LT, value);
-
-      handleComparisonExpression(filterExpression);
-
-      return this;
-    }
-
-    @Override
-    public Builder lessThan(String key, Date value) {
-      AttributeReference ar = new AttributeReference(key);
-      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.LT, value);
-
-      handleComparisonExpression(filterExpression);
-
-      return this;
-    }
-
-    @Override
-    public Builder lessThan(String key, LocalDate value) {
-      AttributeReference ar = new AttributeReference(key);
-      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.LT, value);
-
-      handleComparisonExpression(filterExpression);
-
-      return this;
-    }
-
-    @Override
-    public Builder lessThan(String key, LocalDateTime value) {
-      AttributeReference ar = new AttributeReference(key);
-      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.LT, value);
-
-      handleComparisonExpression(filterExpression);
-
-      return this;
-    }
-
-    @Override
-    public <T extends Number> Builder lessThanOrEquals(String key, T value) {
-      AttributeReference ar = new AttributeReference(key);
-      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.LE, value);
-
-      handleComparisonExpression(filterExpression);
-
-      return this;
-    }
-
-    @Override
-    public Builder lessThanOrEquals(String key, Date value) {
-      AttributeReference ar = new AttributeReference(key);
-      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.LE, value);
-
-      handleComparisonExpression(filterExpression);
-
-      return this;
-    }
-
-    @Override
-    public Builder lessThanOrEquals(String key, LocalDate value) {
-      AttributeReference ar = new AttributeReference(key);
-      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.LE, value);
-
-      handleComparisonExpression(filterExpression);
-
-      return this;
-    }
-
-    @Override
-    public Builder lessThanOrEquals(String key, LocalDateTime value) {
-      AttributeReference ar = new AttributeReference(key);
-      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.LE, value);
-
-      handleComparisonExpression(filterExpression);
-
-      return this;
-    }
-
-    @Override
-    public Builder not(FilterExpression expression) {
-      GroupExpression groupExpression = new GroupExpression();
-
-      groupExpression.setNot(true);
-      groupExpression.setFilterExpression(expression);
-
-      handleComparisonExpression(groupExpression);
-
-      return this;
-    }
-
-    @Override
-    public Builder attributeHas(String attribute, FilterExpression expression) throws FilterParseException {
-      handleComparisonExpression(ValuePathExpression.fromFilterExpression(attribute, expression));
-
-      return this;
-    }
-  }
-
-  public abstract static class Builder {
-
-    FilterExpression filterExpression;
-
-    Builder() {
-    }
-
-    public abstract Builder and();
-    
-    public abstract Builder and(FilterExpression fe1);
-
-    public abstract Builder and(FilterExpression fe1, FilterExpression fe2);
-
-    public abstract Builder or();
-    
-    public abstract Builder or(FilterExpression fe1);
-
-    public abstract Builder or(FilterExpression fe1, FilterExpression fe2);
-
-    public abstract Builder equalTo(String key, String value);
-
-    public abstract Builder equalTo(String key, Boolean value);
-
-    public abstract Builder equalTo(String key, Date value);
-
-    public abstract Builder equalTo(String key, LocalDate value);
-
-    public abstract Builder equalTo(String key, LocalDateTime value);
-
-    public abstract <T extends Number> Builder equalTo(String key, T value);
-
-    public abstract Builder equalNull(String key);
-
-    public abstract Builder notEqual(String key, String value);
-
-    public abstract Builder notEqual(String key, Boolean value);
-
-    public abstract Builder notEqual(String key, Date value);
-
-    public abstract Builder notEqual(String key, LocalDate value);
-
-    public abstract Builder notEqual(String key, LocalDateTime value);
-
-    public abstract <T extends Number> Builder notEqual(String key, T value);
-
-    public abstract Builder notEqualNull(String key);
-
-    public abstract <T extends Number> Builder greaterThan(String key, T value);
-
-    public abstract Builder greaterThan(String key, Date value);
-
-    public abstract Builder greaterThan(String key, LocalDate value);
-
-    public abstract Builder greaterThan(String key, LocalDateTime value);
-
-    public abstract <T extends Number> Builder greaterThanOrEquals(String key, T value);
-
-    public abstract Builder greaterThanOrEquals(String key, Date value);
-
-    public abstract Builder greaterThanOrEquals(String key, LocalDate value);
-
-    public abstract Builder greaterThanOrEquals(String key, LocalDateTime value);
-
-    public abstract <T extends Number> Builder lessThan(String key, T value);
-
-    public abstract Builder lessThan(String key, Date value);
-
-    public abstract Builder lessThan(String key, LocalDate value);
-
-    public abstract Builder lessThan(String key, LocalDateTime value);
-
-    public abstract <T extends Number> Builder lessThanOrEquals(String key, T value);
-
-    public abstract Builder lessThanOrEquals(String key, Date value);
-
-    public abstract Builder lessThanOrEquals(String key, LocalDate value);
-
-    public abstract Builder lessThanOrEquals(String key, LocalDateTime value);
-
-    public abstract Builder endsWith(String key, String value);
-
-    public abstract Builder startsWith(String key, String value);
-
-    public abstract Builder contains(String key, String value);
-
-    public abstract Builder not(FilterExpression filter);
-
-    public abstract Builder attributeHas(String attribute, FilterExpression filter) throws FilterParseException;
-
-    protected Builder handleLogicalExpression(LogicalExpression expression, LogicalOperator operator) {
-      log.info("In handleLogicalExpression");
-      if (filterExpression == null) {
-        filterExpression = expression;
-      } else if (filterExpression instanceof AttributeComparisonExpression) {
-        log.info("Adding a logical expression as the new root");
-
-        log.info("Setting as left: " + filterExpression.toFilter());
-        expression.setLeft(filterExpression);
-        log.info("Setting as right: " + expression.toFilter());
-
-        filterExpression = expression;
-      } else if (filterExpression instanceof LogicalExpression) {
-        log.info("filter exression is a logical expression");
-        LogicalExpression le = (LogicalExpression) filterExpression;
-
-        if (le.getLeft() == null) {
-          log.info("Setting left to: " + expression.toFilter());
-          le.setLeft(expression);
-        } else if (le.getRight() == null) {
-          log.info("Setting right to: " + expression.toFilter());
-          le.setRight(expression);
-        } else {
-          log.info("The current base is complete, raising up one level");
-          LogicalExpression newRoot = new LogicalExpression();
-          log.info("Setting left to: " + expression);
-          newRoot.setLeft(expression);
-          filterExpression = newRoot;
-        }
-      } else if (filterExpression instanceof GroupExpression) {
-        log.info("Found group expression");
-        LogicalExpression newRoot = new LogicalExpression();
-        newRoot.setLeft(filterExpression);
-        newRoot.setRight(expression);
-        newRoot.setOperator(operator);
-        filterExpression = newRoot;
-      }
-
-      log.info("New filter expression: " + filterExpression.toFilter());
-
-      return this;
-    }
-
-    protected void handleComparisonExpression(FilterExpression expression) {
-
-      if (expression == null) {
-        log.error("*** in handle comparison ---> expression == null");
-      }
-      
-      if (filterExpression == null) {
-        filterExpression = expression;
-      } else {
-        if (!(filterExpression instanceof LogicalExpression)) {
-          throw new IllegalStateException();
-        }
-
-        LogicalExpression le = (LogicalExpression) filterExpression;
-        le.setRight(expression);
-      }
-    }
-
-    @Override
-    public String toString() {
-      
-      String filterString = filterExpression.toFilter();
-      try {
-        return URLEncoder.encode(filterString, "UTF-8").replace("+", "%20");
-      } catch (UnsupportedEncodingException e) {
-        log.error("Unsupported encoding:", e);
-        return null;
-      }
-    }
-    
-    public Filter build() {
-      return new Filter(filterExpression);
-    }
-
-    public FilterExpression filter() {
-      return filterExpression;
-    }
-  }
-
-  public static Builder builder() {
-    return new FilterClient.ComparisonBuilder();
-  }
-}
diff --git a/scim-client/src/main/java/org/apache/directory/scim/client/rest/ComparisonBuilder.java b/scim-client/src/main/java/org/apache/directory/scim/client/rest/ComparisonBuilder.java
new file mode 100644
index 0000000..8f08bdc
--- /dev/null
+++ b/scim-client/src/main/java/org/apache/directory/scim/client/rest/ComparisonBuilder.java
@@ -0,0 +1,363 @@
+package org.apache.directory.scim.client.rest;
+
+import org.apache.directory.scim.spec.protocol.attribute.AttributeReference;
+import org.apache.directory.scim.spec.protocol.filter.*;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.Date;
+
+class ComparisonBuilder extends ComplexLogicalBuilder {
+
+    @Override
+    public FilterBuilder equalTo(String key, String value) {
+
+      AttributeReference ar = new AttributeReference(key);
+      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.EQ, value);
+
+      handleComparisonExpression(filterExpression);
+
+      return this;
+    }
+
+    @Override
+    public FilterBuilder equalTo(String key, Boolean value) {
+
+      AttributeReference ar = new AttributeReference(key);
+      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.EQ, value);
+
+      handleComparisonExpression(filterExpression);
+
+      return this;
+    }
+
+    @Override
+    public FilterBuilder equalTo(String key, Date value) {
+      AttributeReference ar = new AttributeReference(key);
+      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.EQ, value);
+
+      handleComparisonExpression(filterExpression);
+
+      return this;
+    }
+
+    @Override
+    public FilterBuilder equalTo(String key, LocalDate value) {
+
+      AttributeReference ar = new AttributeReference(key);
+      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.EQ, value);
+
+      handleComparisonExpression(filterExpression);
+
+      return this;
+    }
+
+    @Override
+    public FilterBuilder equalTo(String key, LocalDateTime value) {
+      AttributeReference ar = new AttributeReference(key);
+      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.EQ, value);
+
+      handleComparisonExpression(filterExpression);
+
+      return this;
+    }
+
+    @Override
+    public <T extends Number> FilterBuilder equalTo(String key, T value) {
+      AttributeReference ar = new AttributeReference(key);
+      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.EQ, value);
+
+      handleComparisonExpression(filterExpression);
+
+      return this;
+    }
+
+    @Override
+    public FilterBuilder equalNull(String key) {
+      AttributeReference ar = new AttributeReference(key);
+      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.EQ, null);
+
+      handleComparisonExpression(filterExpression);
+
+      return this;
+    }
+
+    @Override
+    public FilterBuilder notEqual(String key, String value) {
+      AttributeReference ar = new AttributeReference(key);
+      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.NE, value);
+
+      handleComparisonExpression(filterExpression);
+
+      return this;
+    }
+
+    @Override
+    public FilterBuilder notEqual(String key, Boolean value) {
+      AttributeReference ar = new AttributeReference(key);
+      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.NE, value);
+
+      handleComparisonExpression(filterExpression);
+
+      return this;
+    }
+
+    @Override
+    public FilterBuilder notEqual(String key, Date value) {
+      AttributeReference ar = new AttributeReference(key);
+      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.NE, value);
+
+      handleComparisonExpression(filterExpression);
+
+      return this;
+    }
+
+    @Override
+    public FilterBuilder notEqual(String key, LocalDate value) {
+      AttributeReference ar = new AttributeReference(key);
+      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.NE, value);
+
+      handleComparisonExpression(filterExpression);
+
+      return this;
+    }
+
+    @Override
+    public FilterBuilder notEqual(String key, LocalDateTime value) {
+      AttributeReference ar = new AttributeReference(key);
+      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.NE, value);
+
+      handleComparisonExpression(filterExpression);
+
+      return this;
+    }
+
+    @Override
+    public <T extends Number> FilterBuilder notEqual(String key, T value) {
+      AttributeReference ar = new AttributeReference(key);
+      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.NE, value);
+
+      handleComparisonExpression(filterExpression);
+
+      return this;
+    }
+
+    @Override
+    public FilterBuilder notEqualNull(String key) {
+      AttributeReference ar = new AttributeReference(key);
+      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.NE, null);
+
+      handleComparisonExpression(filterExpression);
+
+      return this;
+    }
+
+    @Override
+    public FilterBuilder endsWith(String key, String value) {
+      AttributeReference ar = new AttributeReference(key);
+      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.EW, value);
+
+      handleComparisonExpression(filterExpression);
+
+      return this;
+    }
+
+    @Override
+    public FilterBuilder startsWith(String key, String value) {
+      AttributeReference ar = new AttributeReference(key);
+      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.SW, value);
+
+      handleComparisonExpression(filterExpression);
+
+      return this;
+    }
+
+    @Override
+    public FilterBuilder contains(String key, String value) {
+      AttributeReference ar = new AttributeReference(key);
+      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.CO, value);
+
+      handleComparisonExpression(filterExpression);
+
+      return this;
+    }
+
+    @Override
+    public <T extends Number> FilterBuilder greaterThan(String key, T value) {
+      AttributeReference ar = new AttributeReference(key);
+      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.GT, value);
+
+      handleComparisonExpression(filterExpression);
+
+      return this;
+    }
+
+    @Override
+    public FilterBuilder greaterThan(String key, Date value) {
+      AttributeReference ar = new AttributeReference(key);
+      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.GT, value);
+
+      handleComparisonExpression(filterExpression);
+
+      return this;
+    }
+
+    @Override
+    public FilterBuilder greaterThan(String key, LocalDate value) {
+      AttributeReference ar = new AttributeReference(key);
+      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.GT, value);
+
+      handleComparisonExpression(filterExpression);
+
+      return this;
+    }
+
+    @Override
+    public FilterBuilder greaterThan(String key, LocalDateTime value) {
+      AttributeReference ar = new AttributeReference(key);
+      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.GT, value);
+
+      handleComparisonExpression(filterExpression);
+
+      return this;
+    }
+
+    @Override
+    public <T extends Number> FilterBuilder greaterThanOrEquals(String key, T value) {
+      AttributeReference ar = new AttributeReference(key);
+      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.GE, value);
+
+      handleComparisonExpression(filterExpression);
+
+      return this;
+    }
+
+    @Override
+    public FilterBuilder greaterThanOrEquals(String key, Date value) {
+      AttributeReference ar = new AttributeReference(key);
+      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.GE, value);
+
+      handleComparisonExpression(filterExpression);
+
+      return this;
+    }
+
+    @Override
+    public FilterBuilder greaterThanOrEquals(String key, LocalDate value) {
+      AttributeReference ar = new AttributeReference(key);
+      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.GE, value);
+
+      handleComparisonExpression(filterExpression);
+
+      return this;
+    }
+
+    @Override
+    public FilterBuilder greaterThanOrEquals(String key, LocalDateTime value) {
+      AttributeReference ar = new AttributeReference(key);
+      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.GE, value);
+
+      handleComparisonExpression(filterExpression);
+
+      return this;
+    }
+
+    @Override
+    public <T extends Number> FilterBuilder lessThan(String key, T value) {
+      AttributeReference ar = new AttributeReference(key);
+      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.LT, value);
+
+      handleComparisonExpression(filterExpression);
+
+      return this;
+    }
+
+    @Override
+    public FilterBuilder lessThan(String key, Date value) {
+      AttributeReference ar = new AttributeReference(key);
+      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.LT, value);
+
+      handleComparisonExpression(filterExpression);
+
+      return this;
+    }
+
+    @Override
+    public FilterBuilder lessThan(String key, LocalDate value) {
+      AttributeReference ar = new AttributeReference(key);
+      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.LT, value);
+
+      handleComparisonExpression(filterExpression);
+
+      return this;
+    }
+
+    @Override
+    public FilterBuilder lessThan(String key, LocalDateTime value) {
+      AttributeReference ar = new AttributeReference(key);
+      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.LT, value);
+
+      handleComparisonExpression(filterExpression);
+
+      return this;
+    }
+
+    @Override
+    public <T extends Number> FilterBuilder lessThanOrEquals(String key, T value) {
+      AttributeReference ar = new AttributeReference(key);
+      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.LE, value);
+
+      handleComparisonExpression(filterExpression);
+
+      return this;
+    }
+
+    @Override
+    public FilterBuilder lessThanOrEquals(String key, Date value) {
+      AttributeReference ar = new AttributeReference(key);
+      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.LE, value);
+
+      handleComparisonExpression(filterExpression);
+
+      return this;
+    }
+
+    @Override
+    public FilterBuilder lessThanOrEquals(String key, LocalDate value) {
+      AttributeReference ar = new AttributeReference(key);
+      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.LE, value);
+
+      handleComparisonExpression(filterExpression);
+
+      return this;
+    }
+
+    @Override
+    public FilterBuilder lessThanOrEquals(String key, LocalDateTime value) {
+      AttributeReference ar = new AttributeReference(key);
+      FilterExpression filterExpression = new AttributeComparisonExpression(ar, CompareOperator.LE, value);
+
+      handleComparisonExpression(filterExpression);
+
+      return this;
+    }
+
+    @Override
+    public FilterBuilder not(FilterExpression expression) {
+      GroupExpression groupExpression = new GroupExpression();
+
+      groupExpression.setNot(true);
+      groupExpression.setFilterExpression(expression);
+
+      handleComparisonExpression(groupExpression);
+
+      return this;
+    }
+
+    @Override
+    public FilterBuilder attributeHas(String attribute, FilterExpression expression) throws FilterParseException {
+      handleComparisonExpression(ValuePathExpression.fromFilterExpression(attribute, expression));
+
+      return this;
+    }
+  }
diff --git a/scim-client/src/main/java/org/apache/directory/scim/client/rest/ComplexLogicalBuilder.java b/scim-client/src/main/java/org/apache/directory/scim/client/rest/ComplexLogicalBuilder.java
new file mode 100644
index 0000000..7b8e333
--- /dev/null
+++ b/scim-client/src/main/java/org/apache/directory/scim/client/rest/ComplexLogicalBuilder.java
@@ -0,0 +1,65 @@
+package org.apache.directory.scim.client.rest;
+
+import org.apache.directory.scim.spec.protocol.filter.AttributeComparisonExpression;
+import org.apache.directory.scim.spec.protocol.filter.FilterExpression;
+import org.apache.directory.scim.spec.protocol.filter.LogicalExpression;
+import org.apache.directory.scim.spec.protocol.filter.LogicalOperator;
+
+abstract class ComplexLogicalBuilder extends SimpleLogicalBuilder {
+
+    @Override
+    public FilterBuilder or(FilterExpression fe1) {
+      if (filterExpression instanceof AttributeComparisonExpression) {
+        LogicalExpression logicalExpression = new LogicalExpression();
+        logicalExpression.setLeft(groupIfNeeded(filterExpression));
+        logicalExpression.setRight(groupIfNeeded(fe1));
+        logicalExpression.setOperator(LogicalOperator.OR);
+        filterExpression = logicalExpression;
+        return this;
+      }
+      
+      LogicalExpression logicalExpression = new LogicalExpression();
+      logicalExpression.setLeft(fe1);
+      logicalExpression.setOperator(LogicalOperator.OR);
+
+      return handleLogicalExpression(logicalExpression, LogicalOperator.OR);
+    }
+    
+    @Override
+    public FilterBuilder or(FilterExpression fe1, FilterExpression fe2) {
+      LogicalExpression logicalExpression = new LogicalExpression();
+      logicalExpression.setLeft(groupIfNeeded(fe1));
+      logicalExpression.setRight(groupIfNeeded(fe2));
+      logicalExpression.setOperator(LogicalOperator.OR);
+
+      return handleLogicalExpression(logicalExpression, LogicalOperator.OR);
+    }
+
+    @Override
+    public FilterBuilder and(FilterExpression fe1, FilterExpression fe2) {
+      LogicalExpression logicalExpression = new LogicalExpression();
+      logicalExpression.setLeft(groupIfNeeded(fe1));
+      logicalExpression.setRight(groupIfNeeded(fe2));
+      logicalExpression.setOperator(LogicalOperator.AND);
+
+      return handleLogicalExpression(logicalExpression, LogicalOperator.AND);
+    }
+    
+    @Override
+    public FilterBuilder and(FilterExpression fe1) {
+      if (filterExpression instanceof AttributeComparisonExpression) {
+        LogicalExpression logicalExpression = new LogicalExpression();
+        logicalExpression.setLeft(filterExpression);
+        logicalExpression.setRight(groupIfNeeded(fe1));
+        logicalExpression.setOperator(LogicalOperator.AND);
+        filterExpression = logicalExpression;
+        return this;
+      }
+      
+      LogicalExpression logicalExpression = new LogicalExpression();
+      logicalExpression.setLeft(fe1);
+      logicalExpression.setOperator(LogicalOperator.AND);
+
+      return handleLogicalExpression(logicalExpression, LogicalOperator.AND);
+    }
+  }
diff --git a/scim-client/src/main/java/org/apache/directory/scim/client/rest/FilterBuilder.java b/scim-client/src/main/java/org/apache/directory/scim/client/rest/FilterBuilder.java
new file mode 100644
index 0000000..e25e6f7
--- /dev/null
+++ b/scim-client/src/main/java/org/apache/directory/scim/client/rest/FilterBuilder.java
@@ -0,0 +1,102 @@
+package org.apache.directory.scim.client.rest;
+
+import org.apache.directory.scim.spec.protocol.filter.FilterExpression;
+import org.apache.directory.scim.spec.protocol.filter.FilterParseException;
+import org.apache.directory.scim.spec.protocol.search.Filter;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.Date;
+
+public interface FilterBuilder {
+
+  FilterBuilder and();
+
+  FilterBuilder and(FilterExpression fe1);
+
+  FilterBuilder and(FilterExpression fe1, FilterExpression fe2);
+
+  FilterBuilder or();
+
+  FilterBuilder or(FilterExpression fe1);
+
+  FilterBuilder or(FilterExpression fe1, FilterExpression fe2);
+
+  FilterBuilder equalTo(String key, String value);
+
+  FilterBuilder equalTo(String key, Boolean value);
+
+  FilterBuilder equalTo(String key, Date value);
+
+  FilterBuilder equalTo(String key, LocalDate value);
+
+  FilterBuilder equalTo(String key, LocalDateTime value);
+
+  <T extends Number> FilterBuilder equalTo(String key, T value);
+
+  FilterBuilder equalNull(String key);
+
+  FilterBuilder notEqual(String key, String value);
+
+  FilterBuilder notEqual(String key, Boolean value);
+
+  FilterBuilder notEqual(String key, Date value);
+
+  FilterBuilder notEqual(String key, LocalDate value);
+
+  FilterBuilder notEqual(String key, LocalDateTime value);
+
+  <T extends Number> FilterBuilder notEqual(String key, T value);
+
+  FilterBuilder notEqualNull(String key);
+
+  <T extends Number> FilterBuilder greaterThan(String key, T value);
+
+  FilterBuilder greaterThan(String key, Date value);
+
+  FilterBuilder greaterThan(String key, LocalDate value);
+
+  FilterBuilder greaterThan(String key, LocalDateTime value);
+
+  <T extends Number> FilterBuilder greaterThanOrEquals(String key, T value);
+
+  FilterBuilder greaterThanOrEquals(String key, Date value);
+
+  FilterBuilder greaterThanOrEquals(String key, LocalDate value);
+
+  FilterBuilder greaterThanOrEquals(String key, LocalDateTime value);
+
+  <T extends Number> FilterBuilder lessThan(String key, T value);
+
+  FilterBuilder lessThan(String key, Date value);
+
+  FilterBuilder lessThan(String key, LocalDate value);
+
+  FilterBuilder lessThan(String key, LocalDateTime value);
+
+  <T extends Number> FilterBuilder lessThanOrEquals(String key, T value);
+
+  FilterBuilder lessThanOrEquals(String key, Date value);
+
+  FilterBuilder lessThanOrEquals(String key, LocalDate value);
+
+  FilterBuilder lessThanOrEquals(String key, LocalDateTime value);
+
+  FilterBuilder endsWith(String key, String value);
+
+  FilterBuilder startsWith(String key, String value);
+
+  FilterBuilder contains(String key, String value);
+
+  FilterBuilder not(FilterExpression filter);
+
+  FilterBuilder attributeHas(String attribute, FilterExpression filter) throws FilterParseException;
+
+  FilterExpression filter();
+
+  Filter build();
+
+  static FilterBuilder create() {
+    return new ComparisonBuilder();
+  }
+}
diff --git a/scim-client/src/main/java/org/apache/directory/scim/client/rest/SimpleLogicalBuilder.java b/scim-client/src/main/java/org/apache/directory/scim/client/rest/SimpleLogicalBuilder.java
new file mode 100644
index 0000000..8656089
--- /dev/null
+++ b/scim-client/src/main/java/org/apache/directory/scim/client/rest/SimpleLogicalBuilder.java
@@ -0,0 +1,121 @@
+package org.apache.directory.scim.client.rest;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.directory.scim.spec.protocol.filter.*;
+import org.apache.directory.scim.spec.protocol.search.Filter;
+
+@Slf4j
+abstract class SimpleLogicalBuilder implements FilterBuilder {
+
+  protected FilterExpression filterExpression;
+
+  @Override
+  public FilterBuilder and() {
+    if (filterExpression == null) {
+      throw new IllegalStateException();
+    }
+
+    LogicalExpression logicalExpression = new LogicalExpression();
+    logicalExpression.setLeft(groupIfNeeded(filterExpression));
+    logicalExpression.setOperator(LogicalOperator.AND);
+    filterExpression = logicalExpression;
+
+    return this;
+  }
+
+  @Override
+  public FilterBuilder or() {
+    if (filterExpression == null) {
+      throw new IllegalStateException();
+    }
+
+    LogicalExpression logicalExpression = new LogicalExpression();
+    logicalExpression.setLeft(groupIfNeeded(filterExpression));
+    logicalExpression.setOperator(LogicalOperator.OR);
+    filterExpression = logicalExpression;
+
+    return this;
+  }
+
+  static FilterExpression groupIfNeeded(FilterExpression filterExpression) {
+    return (filterExpression instanceof LogicalExpression)
+      ? new GroupExpression(false, filterExpression)
+      : filterExpression;
+  }
+
+  protected FilterBuilder handleLogicalExpression(LogicalExpression expression, LogicalOperator operator) {
+    log.info("In handleLogicalExpression");
+    if (filterExpression == null) {
+      filterExpression = expression;
+    } else if (filterExpression instanceof AttributeComparisonExpression) {
+      log.info("Adding a logical expression as the new root");
+
+      log.info("Setting as left: " + filterExpression.toFilter());
+      expression.setLeft(filterExpression);
+      log.info("Setting as right: " + expression.toFilter());
+
+      filterExpression = expression;
+    } else if (filterExpression instanceof LogicalExpression) {
+      log.info("filter exression is a logical expression");
+      LogicalExpression le = (LogicalExpression) filterExpression;
+
+      if (le.getLeft() == null) {
+        log.info("Setting left to: " + expression.toFilter());
+        le.setLeft(expression);
+      } else if (le.getRight() == null) {
+        log.info("Setting right to: " + expression.toFilter());
+        le.setRight(expression);
+      } else {
+        log.info("The current base is complete, raising up one level");
+        LogicalExpression newRoot = new LogicalExpression();
+        log.info("Setting left to: " + expression);
+        newRoot.setLeft(expression);
+        filterExpression = newRoot;
+      }
+    } else if (filterExpression instanceof GroupExpression) {
+      log.info("Found group expression");
+      LogicalExpression newRoot = new LogicalExpression();
+      newRoot.setLeft(filterExpression);
+      newRoot.setRight(expression);
+      newRoot.setOperator(operator);
+      filterExpression = newRoot;
+    }
+
+    log.info("New filter expression: " + filterExpression.toFilter());
+
+    return this;
+  }
+
+  protected void handleComparisonExpression(FilterExpression expression) {
+
+    if (expression == null) {
+      log.error("*** in handle comparison ---> expression == null");
+    }
+
+    if (filterExpression == null) {
+      filterExpression = expression;
+    } else {
+      if (!(filterExpression instanceof LogicalExpression)) {
+        throw new IllegalStateException();
+      }
+
+      LogicalExpression le = (LogicalExpression) filterExpression;
+      le.setRight(groupIfNeeded(expression));
+    }
+  }
+
+  @Override
+  public String toString() {
+    return filterExpression.toFilter();
+  }
+
+  @Override
+  public FilterExpression filter() {
+    return filterExpression;
+  }
+
+  @Override
+  public Filter build() {
+    return new Filter(filterExpression);
+  }
+}
diff --git a/scim-client/src/test/java/org/apache/directory/scim/client/filter/FilterBuilderEqualsTest.java b/scim-client/src/test/java/org/apache/directory/scim/client/filter/FilterBuilderEqualsTest.java
new file mode 100644
index 0000000..bf1aef6
--- /dev/null
+++ b/scim-client/src/test/java/org/apache/directory/scim/client/filter/FilterBuilderEqualsTest.java
@@ -0,0 +1,118 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one
+* or more contributor license agreements.  See the NOTICE file
+* distributed with this work for additional information
+* regarding copyright ownership.  The ASF licenses this file
+* to you under the Apache License, Version 2.0 (the
+* "License"); you may not use this file except in compliance
+* with the License.  You may obtain a copy of the License at
+ 
+* http://www.apache.org/licenses/LICENSE-2.0
+
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied.  See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+
+package org.apache.directory.scim.client.filter;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.directory.scim.client.rest.FilterBuilder;
+import org.apache.directory.scim.spec.protocol.filter.FilterParseException;
+import org.apache.directory.scim.spec.protocol.search.Filter;
+import org.junit.jupiter.api.Test;
+
+import java.text.SimpleDateFormat;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Date;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@Slf4j
+public class FilterBuilderEqualsTest {
+  
+  @Test
+  public void testEqualToStringString() throws FilterParseException {
+    Filter filter = FilterBuilder.create().equalTo("address.streetAddress", "7714 Sassafrass Way").build();
+    Filter expected = new Filter("address.streetAddress EQ \"7714 Sassafrass Way\"");
+    assertThat(filter).isEqualTo(expected);
+  }
+
+  @Test
+  public void testEqualToStringBoolean() throws FilterParseException {
+    Filter filter = FilterBuilder.create().equalTo("address.active", true).build();
+    Filter expected = new Filter("address.active EQ true");
+    assertThat(filter).isEqualTo(expected);
+  }
+
+  @Test
+  public void testEqualToStringDate() throws FilterParseException {
+    Date now = new Date();
+    Filter filter = FilterBuilder.create().equalTo("date.date", now).build();
+    Filter expected = new Filter("date.date EQ \"" + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SS").format(now) + "\""); // FIXME: format is missing TZ
+    // TODO: dates are parsed to strings, for now use string comparison
+    assertThat(filter.getFilter()).isEqualTo(expected.getFilter());
+  }
+
+  @Test
+  public void testEqualToStringLocalDate() throws FilterParseException {
+    LocalDate now = LocalDate.now();
+    Filter filter = FilterBuilder.create().equalTo("date.date", now).build();
+    Filter expected = new Filter("date.date EQ \"" + DateTimeFormatter.ISO_LOCAL_DATE.format(now) + "\"");
+    // TODO: dates are parsed to strings, for now use string comparison
+    assertThat(filter.getFilter()).isEqualTo(expected.getFilter());
+  }
+
+  @Test
+  public void testEqualToStringLocalDateTime() throws FilterParseException {
+    LocalDateTime now = LocalDateTime.now();
+    Filter filter = FilterBuilder.create().equalTo("date.date", now).build();
+    Filter expected = new Filter("date.date EQ \"" + DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(now) + "\"");
+    // TODO: dates are parsed to strings, for now use string comparison
+    assertThat(filter.getFilter()).isEqualTo(expected.getFilter());
+  }
+
+  @Test
+  public void testEqualToStringInteger() throws FilterParseException {
+    int i = 10;
+    Filter filter = FilterBuilder.create().equalTo("int.int", i).build();
+    Filter expected = new Filter("int.int EQ 10");
+    assertThat(filter).isEqualTo(expected);
+  }
+
+  @Test
+  public void testEqualToStringLong() throws FilterParseException {
+    long i = 10l;
+    Filter filter = FilterBuilder.create().equalTo("long.long", i).build();
+    Filter expected = new Filter("long.long EQ 10");
+    assertThat(filter.getFilter()).isEqualTo(expected.getFilter());
+  }
+
+  @Test
+  public void testEqualToStringFloat() throws FilterParseException {
+    float i = 10.2f;
+    Filter filter = FilterBuilder.create().equalTo("float.float", i).build();
+    Filter expected = new Filter("float.float EQ 10.2");
+    assertThat(filter.getFilter()).isEqualTo(expected.getFilter());
+  }
+
+  @Test
+  public void testEqualToStringDouble() throws FilterParseException {
+    double i = 10.2;
+    Filter filter = FilterBuilder.create().equalTo("double.double", i).build();
+    Filter expected = new Filter("double.double EQ 10.2");
+    assertThat(filter).isEqualTo(expected);
+  }
+
+  @Test
+  public void testEqualNull() throws FilterParseException {
+    Filter filter = FilterBuilder.create().equalNull("null.null").build();
+    Filter expected = new Filter("null.null EQ null");
+    assertThat(filter).isEqualTo(expected);
+  }
+}
diff --git a/scim-client/src/test/java/org/apache/directory/scim/client/filter/FilterBuilderGreaterTest.java b/scim-client/src/test/java/org/apache/directory/scim/client/filter/FilterBuilderGreaterTest.java
index e93aee2..545c524 100644
--- a/scim-client/src/test/java/org/apache/directory/scim/client/filter/FilterBuilderGreaterTest.java
+++ b/scim-client/src/test/java/org/apache/directory/scim/client/filter/FilterBuilderGreaterTest.java
@@ -19,19 +19,26 @@
 
 package org.apache.directory.scim.client.filter;
 
+import lombok.extern.slf4j.Slf4j;
+import org.apache.directory.scim.client.rest.FilterBuilder;
+import org.apache.directory.scim.spec.protocol.filter.FilterParseException;
+import org.apache.directory.scim.spec.protocol.search.Filter;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
 import java.io.UnsupportedEncodingException;
 import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.text.SimpleDateFormat;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
 import java.util.Date;
 
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.params.ParameterizedTest;
-
-import org.apache.directory.scim.spec.protocol.filter.FilterParseException;
-import org.apache.directory.scim.spec.protocol.search.Filter;
-import lombok.extern.slf4j.Slf4j;
-import org.junit.jupiter.params.provider.MethodSource;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.assertj.core.api.Assertions.assertThat;
 
 @Slf4j
 public class FilterBuilderGreaterTest {
@@ -59,112 +66,124 @@ public class FilterBuilderGreaterTest {
   
   @ParameterizedTest
   @MethodSource("getIntExamples")
-  public void testGreaterThanT_Int(Integer arg) throws UnsupportedEncodingException, FilterParseException {
-    
-    String encoded = FilterClient.builder().greaterThan("dog.weight", arg).toString();
-    new Filter(decode(encoded));
+  public void testGreaterThanT_Int(Integer arg) throws FilterParseException {
+    Filter filter = FilterBuilder.create().greaterThan("dog.weight", arg).build();
+    Filter expected = new Filter("dog.weight GT " + arg);
+    assertThat(filter).isEqualTo(expected);
   }
 
   @ParameterizedTest
   @MethodSource("getLongExamples")
-  public void getLongExamples(Long arg) throws UnsupportedEncodingException, FilterParseException {
-    
-    String encoded = FilterClient.builder().greaterThan("dog.weight", arg).toString();
-    new Filter(decode(encoded));
+  public void getLongExamples(Long arg) throws FilterParseException {
+    Filter filter = FilterBuilder.create().greaterThan("dog.weight", arg).build();
+    Filter expected = new Filter("dog.weight GT " + arg);
+    // values are parsed to integers, use string comparison
+    assertThat(filter.getFilter()).isEqualTo(expected.getFilter());
   }
-  
+
   @ParameterizedTest
   @MethodSource("getFloatExamples")
-  public void testGreaterThanT_Float(Float arg) throws UnsupportedEncodingException, FilterParseException {
-    
-    String encoded = FilterClient.builder().greaterThan("dog.weight", arg).toString();
-    new Filter(decode(encoded));
+  public void testGreaterThanT_Float(Float arg) throws FilterParseException {
+    Filter filter = FilterBuilder.create().greaterThan("dog.weight", arg).build();
+    Filter expected = new Filter("dog.weight GT " + arg);
+    // values are parsed to doubles, use string comparison
+    assertThat(filter.getFilter()).isEqualTo(expected.getFilter());
   }
-  
+
   @ParameterizedTest
   @MethodSource("getDoubleExamples")
-  public void testGreaterThanT_Double(Double arg) throws UnsupportedEncodingException, FilterParseException {
-    
-    String encoded = FilterClient.builder().greaterThan("dog.weight", arg).toString();
-    new Filter(decode(encoded));
+  public void testGreaterThanT_Double(Double arg) throws FilterParseException {
+    Filter filter = FilterBuilder.create().greaterThan("dog.weight", arg).build();
+    Filter expected = new Filter("dog.weight GT " + arg);
+    assertThat(filter).isEqualTo(expected);
   }
-  
+
   @Test
-  public void testGreaterThanDate() throws UnsupportedEncodingException, FilterParseException {
-    String encoded = FilterClient.builder().greaterThan("dog.dob", new Date()).toString();
-    new Filter(decode(encoded));
+  public void testGreaterThanDate() throws FilterParseException {
+    Date now = new Date();
+    Filter filter = FilterBuilder.create().greaterThan("dog.dob", now).build();
+    Filter expected = new Filter("dog.dob GT \"" + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SS").format(now) + "\""); // FIXME: format is missing TZ
+    // TODO: dates are parsed to strings, for now use string comparison
+    assertThat(filter.getFilter()).isEqualTo(expected.getFilter());
   }
 
   @Test
-  public void testGreaterThanLocalDate() throws UnsupportedEncodingException, FilterParseException {
-    String encoded = FilterClient.builder().greaterThan("dog.dob", LocalDate.now()).toString();
-    new Filter(decode(encoded));
+  public void testGreaterThanLocalDate() throws FilterParseException {
+    LocalDate now = LocalDate.now();
+    Filter filter = FilterBuilder.create().greaterThan("dog.dob", now).build();
+    Filter expected = new Filter("dog.dob GT \"" + DateTimeFormatter.ISO_LOCAL_DATE.format(now) + "\"");
+    // TODO: dates are parsed to strings, for now use string comparison
+    assertThat(filter.getFilter()).isEqualTo(expected.getFilter());
   }
 
   @Test
-  public void testGreaterThanLocalDateTime() throws UnsupportedEncodingException, FilterParseException  {
-    String encoded = FilterClient.builder().greaterThan("dog.dob", LocalDateTime.now()).toString();
-    new Filter(decode(encoded));
+  public void testGreaterThanLocalDateTime() throws FilterParseException  {
+    LocalDate now = LocalDate.now();
+    Filter filter = FilterBuilder.create().greaterThan("dog.dob", now).build();
+    Filter expected = new Filter("dog.dob GT \"" + DateTimeFormatter.ISO_LOCAL_DATE.format(now) + "\"");
+    // TODO: dates are parsed to strings, for now use string comparison
+    assertThat(filter.getFilter()).isEqualTo(expected.getFilter());
   }
 
   @ParameterizedTest
   @MethodSource("getIntExamples")
-  public void testGreaterThanOrEqualsT_Int(Integer arg) throws UnsupportedEncodingException, FilterParseException {
-    
-    String encoded = FilterClient.builder().greaterThanOrEquals("dog.weight", arg).toString();
-    new Filter(decode(encoded));
+  public void testGreaterThanOrEqualsT_Int(Integer arg) throws FilterParseException {
+    Filter filter = FilterBuilder.create().greaterThanOrEquals("dog.weight", arg).build();
+    Filter expected = new Filter("dog.weight GE " + arg);
+    assertThat(filter).isEqualTo(expected);
   }
 
   @ParameterizedTest
   @MethodSource("getLongExamples")
-  public void testGreaterThanOrEqualsT_Long(Long arg) throws UnsupportedEncodingException, FilterParseException {
-    
-    String encoded = FilterClient.builder().greaterThanOrEquals("dog.weight", arg).toString();
-    new Filter(decode(encoded));
+  public void testGreaterThanOrEqualsT_Long(Long arg) throws FilterParseException {
+    Filter filter = FilterBuilder.create().greaterThanOrEquals("dog.weight", arg).build();
+    Filter expected = new Filter("dog.weight GE " + arg);
+    // values are parsed to integers, use string comparison
+    assertThat(filter.getFilter()).isEqualTo(expected.getFilter());
   }
-  
+
   @ParameterizedTest
   @MethodSource("getFloatExamples")
-  public void testGreaterThanOrEqualsT_Float(Float arg) throws UnsupportedEncodingException, FilterParseException {
-    
-    String encoded = FilterClient.builder().greaterThanOrEquals("dog.weight", arg).toString();
-    new Filter(decode(encoded));
+  public void testGreaterThanOrEqualsT_Float(Float arg) throws FilterParseException {
+    Filter filter = FilterBuilder.create().greaterThanOrEquals("dog.weight", arg).build();
+    Filter expected = new Filter("dog.weight GE " + arg);
+    // values are parsed to doubles, use string comparison
+    assertThat(filter.getFilter()).isEqualTo(expected.getFilter());
   }
-  
+
   @ParameterizedTest
   @MethodSource("getDoubleExamples")
-  public void testGreaterThanOrEqualsT_Double(Double arg) throws UnsupportedEncodingException, FilterParseException {
-    
-    String encoded = FilterClient.builder().greaterThanOrEquals("dog.weight", arg).toString();
-    new Filter(decode(encoded));
+  public void testGreaterThanOrEqualsT_Double(Double arg) throws FilterParseException {
+    Filter filter = FilterBuilder.create().greaterThanOrEquals("dog.weight", arg).build();
+    Filter expected = new Filter("dog.weight GE " + arg);
+    assertThat(filter).isEqualTo(expected);
   }
-  
+
   @Test
-  public void testGreaterThanOrEqualsDate() throws UnsupportedEncodingException, FilterParseException {
-    String encoded = FilterClient.builder().greaterThanOrEquals("dog.dob", new Date()).toString();
-    new Filter(decode(encoded));
+  public void testGreaterThanOrEqualsDate() throws FilterParseException {
+    Date now = new Date();
+    Filter filter = FilterBuilder.create().greaterThanOrEquals("dog.dob", now).build();
+    Filter expected = new Filter("dog.dob GE \"" + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SS").format(now) + "\""); // FIXME: format is missing TZ
+    // TODO: dates are parsed to strings, for now use string comparison
+    assertThat(filter.getFilter()).isEqualTo(expected.getFilter());
   }
 
   @Test
-  public void testGreaterThanOrEqualsLocalDate()  throws UnsupportedEncodingException, FilterParseException {
-    String encoded = FilterClient.builder().greaterThanOrEquals("dog.dob", LocalDate.now()).toString();
-    new Filter(decode(encoded));
+  public void testGreaterThanOrEqualsLocalDate()  throws FilterParseException {
+    LocalDate now = LocalDate.now();
+    Filter filter = FilterBuilder.create().greaterThanOrEquals("dog.dob", now).build();
+    Filter expected = new Filter("dog.dob GE \"" + DateTimeFormatter.ISO_LOCAL_DATE.format(now) + "\"");
+    // TODO: dates are parsed to strings, for now use string comparison
+    assertThat(filter.getFilter()).isEqualTo(expected.getFilter());
   }
 
   @Test
-  public void testGreaterThanOrEqualsLocalDateTime() throws UnsupportedEncodingException, FilterParseException {
-    String encoded = FilterClient.builder().greaterThanOrEquals("dog.dob", LocalDateTime.now()).toString();
-    new Filter(decode(encoded));
+  public void testGreaterThanOrEqualsLocalDateTime() throws FilterParseException {
+    LocalDateTime now = LocalDateTime.now();
+    Filter filter = FilterBuilder.create().greaterThanOrEquals("dog.dob", now).build();
+    Filter expected = new Filter("dog.dob GE \"" + DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(now) + "\"");
+    // TODO: dates are parsed to strings, for now use string comparison
+    assertThat(filter.getFilter()).isEqualTo(expected.getFilter());
   }
 
-  private String decode(String encoded) throws UnsupportedEncodingException {
-
-    log.info(encoded);
-    
-    String decoded = URLDecoder.decode(encoded, "UTF-8").replace("%20", " ");
-    
-    log.info(decoded);
-    
-    return decoded;
-  }
 }
diff --git a/scim-client/src/test/java/org/apache/directory/scim/client/filter/FilterBuilderLessThanTest.java b/scim-client/src/test/java/org/apache/directory/scim/client/filter/FilterBuilderLessThanTest.java
index 8b45342..20bc3d4 100644
--- a/scim-client/src/test/java/org/apache/directory/scim/client/filter/FilterBuilderLessThanTest.java
+++ b/scim-client/src/test/java/org/apache/directory/scim/client/filter/FilterBuilderLessThanTest.java
@@ -19,19 +19,23 @@
 
 package org.apache.directory.scim.client.filter;
 
-import java.io.UnsupportedEncodingException;
-import java.net.URLDecoder;
-import java.time.LocalDate;
-import java.time.LocalDateTime;
-import java.util.Date;
-
+import lombok.extern.slf4j.Slf4j;
+import org.apache.directory.scim.client.rest.FilterBuilder;
 import org.apache.directory.scim.spec.protocol.filter.FilterParseException;
 import org.apache.directory.scim.spec.protocol.search.Filter;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.MethodSource;
 
-import lombok.extern.slf4j.Slf4j;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.text.SimpleDateFormat;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Date;
+
+import static org.assertj.core.api.Assertions.assertThat;
 
 @Slf4j
 public class FilterBuilderLessThanTest {
@@ -59,113 +63,125 @@ public class FilterBuilderLessThanTest {
   
   @ParameterizedTest
   @MethodSource("getIntExamples")
-  public void testLessThanT_Int(Integer arg) throws UnsupportedEncodingException, FilterParseException {
-    
-    String encoded = FilterClient.builder().lessThan("dog.weight", arg).toString();
-    new Filter(decode(encoded));
+  public void testLessThanT_Int(Integer arg) throws FilterParseException {
+    Filter filter = FilterBuilder.create().lessThan("dog.weight", arg).build();
+    Filter expected = new Filter("dog.weight LT " + arg);
+    assertThat(filter).isEqualTo(expected);
   }
 
   @ParameterizedTest
   @MethodSource("getLongExamples")
-  public void testLessThanT_Long(Long arg) throws UnsupportedEncodingException, FilterParseException {
-    
-    String encoded = FilterClient.builder().lessThan("dog.weight", arg).toString();
-    new Filter(decode(encoded));
+  public void testLessThanT_Long(Long arg) throws FilterParseException {
+    Filter filter = FilterBuilder.create().lessThan("dog.weight", arg).build();
+    Filter expected = new Filter("dog.weight LT " + arg);
+    // values are parsed to integers, use string comparison
+    assertThat(filter.getFilter()).isEqualTo(expected.getFilter());
   }
   
   @ParameterizedTest
   @MethodSource("getFloatExamples")
-  public void testLessThanT_Float(Float arg) throws UnsupportedEncodingException, FilterParseException {
-    
-    String encoded = FilterClient.builder().lessThan("dog.weight", arg).toString();
-    new Filter(decode(encoded));
+  public void testLessThanT_Float(Float arg) throws FilterParseException {
+    Filter filter = FilterBuilder.create().lessThan("dog.weight", arg).build();
+    Filter expected = new Filter("dog.weight LT " + arg);
+    // values are parsed to doubles, use string comparison
+    assertThat(filter.getFilter()).isEqualTo(expected.getFilter());
   }
   
   @ParameterizedTest
   @MethodSource("getDoubleExamples")
-  public void testLessThanT_Double(Double arg) throws UnsupportedEncodingException, FilterParseException {
-    
-    String encoded = FilterClient.builder().lessThan("dog.weight", arg).toString();
-    new Filter(decode(encoded));
+  public void testLessThanT_Double(Double arg) throws FilterParseException {
+    Filter filter = FilterBuilder.create().lessThan("dog.weight", arg).build();
+    Filter expected = new Filter("dog.weight LT " + arg);
+    assertThat(filter).isEqualTo(expected);
   }
   
   
   @Test
-  public void testLessThanDate() throws UnsupportedEncodingException, FilterParseException {
-    String encoded = FilterClient.builder().lessThan("dog.dob", new Date()).toString();
-    new Filter(decode(encoded));
+  public void testLessThanDate() throws FilterParseException {
+    Date now = new Date();
+    Filter filter = FilterBuilder.create().lessThan("dog.dob", new Date()).build();
+    Filter expected = new Filter("dog.dob LT \"" + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SS").format(now) + "\""); // FIXME: format is missing TZ
+    // TODO: dates are parsed to strings, for now use string comparison
+    assertThat(filter.getFilter()).isEqualTo(expected.getFilter());
   }
 
   @Test
-  public void testLessThanLocalDate() throws UnsupportedEncodingException, FilterParseException {
-    String encoded = FilterClient.builder().lessThan("dog.dob", LocalDate.now()).toString();
-    new Filter(decode(encoded));
+  public void testLessThanLocalDate() throws FilterParseException {
+    LocalDate now = LocalDate.now();
+    Filter filter = FilterBuilder.create().lessThan("dog.dob", now).build();
+    Filter expected = new Filter("dog.dob LT \"" + DateTimeFormatter.ISO_LOCAL_DATE.format(now) + "\"");
+    // TODO: dates are parsed to strings, for now use string comparison
+    assertThat(filter.getFilter()).isEqualTo(expected.getFilter());
   }
 
   @Test
-  public void testLessThanLocalDateTime() throws UnsupportedEncodingException, FilterParseException  {
-    String encoded = FilterClient.builder().lessThan("dog.dob", LocalDateTime.now()).toString();
-    new Filter(decode(encoded));
+  public void testLessThanLocalDateTime() throws FilterParseException  {
+    LocalDateTime now = LocalDateTime.now();
+    Filter filter = FilterBuilder.create().lessThan("dog.dob", now).build();
+    Filter expected = new Filter("dog.dob LT \"" + DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(now) + "\"");
+    // TODO: dates are parsed to strings, for now use string comparison
+    assertThat(filter.getFilter()).isEqualTo(expected.getFilter());
   }
 
   @ParameterizedTest
   @MethodSource("getIntExamples")
-  public void testLessThanOrEqualsT_Int(Integer arg) throws UnsupportedEncodingException, FilterParseException {
-    
-    String encoded = FilterClient.builder().lessThanOrEquals("dog.weight", arg).toString();
-    new Filter(decode(encoded));
+  public void testLessThanOrEqualsT_Int(Integer arg) throws FilterParseException {
+    Filter filter = FilterBuilder.create().lessThanOrEquals("dog.weight", arg).build();
+    Filter expected = new Filter("dog.weight LE " + arg);
+    assertThat(filter).isEqualTo(expected);
   }
 
   @ParameterizedTest
   @MethodSource("getLongExamples")
-  public void testLessThanOrEqualsT_Long(Long arg) throws UnsupportedEncodingException, FilterParseException {
-    
-    String encoded = FilterClient.builder().lessThanOrEquals("dog.weight", arg).toString();
-    new Filter(decode(encoded));
+  public void testLessThanOrEqualsT_Long(Long arg) throws FilterParseException {
+    Filter filter = FilterBuilder.create().lessThanOrEquals("dog.weight", arg).build();
+    Filter expected = new Filter("dog.weight LE " + arg);
+    // values are parsed to doubles, use string comparison
+    assertThat(filter.getFilter()).isEqualTo(expected.getFilter());
   }
   
   @ParameterizedTest
   @MethodSource("getFloatExamples")
-  public void testLessThanOrEqualsT_Float(Float arg) throws UnsupportedEncodingException, FilterParseException {
-    
-    String encoded = FilterClient.builder().lessThanOrEquals("dog.weight", arg).toString();
-    new Filter(decode(encoded));
+  public void testLessThanOrEqualsT_Float(Float arg) throws FilterParseException {
+    Filter filter = FilterBuilder.create().lessThanOrEquals("dog.weight", arg).build();
+    Filter expected = new Filter("dog.weight LE " + arg);
+    // values are parsed to doubles, use string comparison
+    assertThat(filter.getFilter()).isEqualTo(expected.getFilter());
   }
   
   @ParameterizedTest
   @MethodSource("getDoubleExamples")
-  public void testLessThanOrEqualsT_Double(Double arg) throws UnsupportedEncodingException, FilterParseException {
-    
-    String encoded = FilterClient.builder().lessThanOrEquals("dog.weight", arg).toString();
-    new Filter(decode(encoded));
+  public void testLessThanOrEqualsT_Double(Double arg) throws FilterParseException {
+    Filter filter = FilterBuilder.create().lessThanOrEquals("dog.weight", arg).build();
+    Filter expected = new Filter("dog.weight LE " + arg);
+    // values are parsed to doubles, use string comparison
+    assertThat(filter.getFilter()).isEqualTo(expected.getFilter());
   }
   
   @Test
-  public void testLessThanOrEqualsDate() throws UnsupportedEncodingException, FilterParseException {
-    String encoded = FilterClient.builder().lessThanOrEquals("dog.dob", new Date()).toString();
-    new Filter(decode(encoded));
+  public void testLessThanOrEqualsDate() throws FilterParseException {
+    Date now = new Date();
+    Filter filter = FilterBuilder.create().lessThanOrEquals("dog.dob", now).build();
+    Filter expected = new Filter("dog.dob LE \"" + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SS").format(now) + "\""); // FIXME: format is missing TZ
+    // TODO: dates are parsed to strings, for now use string comparison
+    assertThat(filter.getFilter()).isEqualTo(expected.getFilter());
   }
 
   @Test
-  public void testLessThanOrEqualsLocalDate()  throws UnsupportedEncodingException, FilterParseException {
-    String encoded = FilterClient.builder().lessThanOrEquals("dog.dob", LocalDate.now()).toString();
-    new Filter(decode(encoded));
+  public void testLessThanOrEqualsLocalDate()  throws FilterParseException {
+    LocalDate now = LocalDate.now();
+    Filter filter = FilterBuilder.create().lessThanOrEquals("dog.dob", now).build();
+    Filter expected = new Filter("dog.dob LE \"" + DateTimeFormatter.ISO_LOCAL_DATE.format(now) + "\"");
+    // TODO: dates are parsed to strings, for now use string comparison
+    assertThat(filter.getFilter()).isEqualTo(expected.getFilter());
   }
 
   @Test
-  public void testLessThanOrEqualsLocalDateTime() throws UnsupportedEncodingException, FilterParseException {
-    String encoded = FilterClient.builder().lessThanOrEquals("dog.dob", LocalDateTime.now()).toString();
-    new Filter(decode(encoded));
-  }
-
-  private String decode(String encoded) throws UnsupportedEncodingException {
-
-    log.info(encoded);
-    
-    String decoded = URLDecoder.decode(encoded, "UTF-8").replace("%20", " ");
-    
-    log.info(decoded);
-    
-    return decoded;
+  public void testLessThanOrEqualsLocalDateTime() throws FilterParseException {
+    LocalDateTime now = LocalDateTime.now();
+    Filter filter = FilterBuilder.create().lessThanOrEquals("dog.dob", now).build();
+    Filter expected = new Filter("dog.dob LE \"" + DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(now) + "\"");
+    // TODO: dates are parsed to strings, for now use string comparison
+    assertThat(filter.getFilter()).isEqualTo(expected.getFilter());
   }
 }
diff --git a/scim-client/src/test/java/org/apache/directory/scim/client/filter/FilterBuilderNotEqualsTest.java b/scim-client/src/test/java/org/apache/directory/scim/client/filter/FilterBuilderNotEqualsTest.java
new file mode 100644
index 0000000..c540130
--- /dev/null
+++ b/scim-client/src/test/java/org/apache/directory/scim/client/filter/FilterBuilderNotEqualsTest.java
@@ -0,0 +1,119 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one
+* or more contributor license agreements.  See the NOTICE file
+* distributed with this work for additional information
+* regarding copyright ownership.  The ASF licenses this file
+* to you under the Apache License, Version 2.0 (the
+* "License"); you may not use this file except in compliance
+* with the License.  You may obtain a copy of the License at
+ 
+* http://www.apache.org/licenses/LICENSE-2.0
+
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied.  See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+
+package org.apache.directory.scim.client.filter;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.directory.scim.client.rest.FilterBuilder;
+import org.apache.directory.scim.spec.protocol.filter.FilterParseException;
+import org.apache.directory.scim.spec.protocol.search.Filter;
+import org.junit.jupiter.api.Test;
+
+import java.io.UnsupportedEncodingException;
+import java.text.SimpleDateFormat;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Date;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@Slf4j
+public class FilterBuilderNotEqualsTest {
+
+  @Test
+  public void testNotnotEqualStringString() throws FilterParseException {
+    Filter filter = FilterBuilder.create().notEqual("address.streetAddress", "7714 Sassafrass Way").build();
+    Filter expected = new Filter("address.streetAddress NE \"7714 Sassafrass Way\"");
+    assertThat(filter).isEqualTo(expected);
+  }
+
+  @Test
+  public void testNotnotEqualStringBoolean() throws FilterParseException {
+    Filter filter = FilterBuilder.create().notEqual("address.active", true).build();
+    Filter expected = new Filter("address.active NE true");
+    assertThat(filter).isEqualTo(expected);
+  }
+
+  @Test
+  public void testNotnotEqualStringDate() throws FilterParseException {
+    Date now = new Date();
+    Filter filter = FilterBuilder.create().notEqual("date.date", now).build();
+    Filter expected = new Filter("date.date NE \"" + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SS").format(now) + "\""); // FIXME: format is missing TZ
+    // TODO: dates are parsed to strings, for now use string comparison
+    assertThat(filter.getFilter()).isEqualTo(expected.getFilter());
+  }
+
+  @Test
+  public void testNotnotEqualStringLocalDate() throws FilterParseException {
+    LocalDate now = LocalDate.now();
+    Filter filter = FilterBuilder.create().notEqual("date.date", now).build();
+    Filter expected = new Filter("date.date NE \"" + DateTimeFormatter.ISO_LOCAL_DATE.format(now) + "\"");
+    // TODO: dates are parsed to strings, for now use string comparison
+    assertThat(filter.getFilter()).isEqualTo(expected.getFilter());
+  }
+
+  @Test
+  public void testNotnotEqualStringLocalDateTime() throws FilterParseException {
+    LocalDateTime now = LocalDateTime.now();
+    Filter filter = FilterBuilder.create().notEqual("date.date", now).build();
+    Filter expected = new Filter("date.date NE \"" + DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(now) + "\"");
+    // TODO: dates are parsed to strings, for now use string comparison
+    assertThat(filter.getFilter()).isEqualTo(expected.getFilter());
+  }
+
+  @Test
+  public void testNotnotEqualStringInteger() throws FilterParseException {
+    int i = 10;
+    Filter filter = FilterBuilder.create().notEqual("int.int", i).build();
+    Filter expected = new Filter("int.int NE 10");
+    assertThat(filter).isEqualTo(expected);
+  }
+
+  @Test
+  public void testNotnotEqualStringLong() throws FilterParseException {
+    long i = 10l;
+    Filter filter = FilterBuilder.create().notEqual("long.long", i).build();
+    Filter expected = new Filter("long.long NE 10");
+    assertThat(filter.getFilter()).isEqualTo(expected.getFilter());
+  }
+
+  @Test
+  public void testNotnotEqualStringFloat() throws FilterParseException {
+    float i = 10.2f;
+    Filter filter = FilterBuilder.create().notEqual("float.float", i).build();
+    Filter expected = new Filter("float.float NE 10.2");
+    assertThat(filter.getFilter()).isEqualTo(expected.getFilter());
+  }
+
+  @Test
+  public void testNotnotEqualStringDouble() throws FilterParseException {
+    double i = 10.2;
+    Filter filter = FilterBuilder.create().notEqual("double.double", i).build();
+    Filter expected = new Filter("double.double NE 10.2");
+    assertThat(filter).isEqualTo(expected);
+  }
+
+  @Test
+  public void testNotEqualNull() throws FilterParseException {
+    Filter filter = FilterBuilder.create().notEqualNull("null.null").build();
+    Filter expected = new Filter("null.null NE null");
+    assertThat(filter).isEqualTo(expected);
+  }
+}
diff --git a/scim-client/src/test/java/org/apache/directory/scim/client/filter/FilterBuilderStringTests.java b/scim-client/src/test/java/org/apache/directory/scim/client/filter/FilterBuilderStringTest.java
similarity index 51%
rename from scim-client/src/test/java/org/apache/directory/scim/client/filter/FilterBuilderStringTests.java
rename to scim-client/src/test/java/org/apache/directory/scim/client/filter/FilterBuilderStringTest.java
index 2703e80..6b6d27d 100644
--- a/scim-client/src/test/java/org/apache/directory/scim/client/filter/FilterBuilderStringTests.java
+++ b/scim-client/src/test/java/org/apache/directory/scim/client/filter/FilterBuilderStringTest.java
@@ -19,44 +19,35 @@
 
 package org.apache.directory.scim.client.filter;
 
-import java.io.UnsupportedEncodingException;
-import java.net.URLDecoder;
-
+import lombok.extern.slf4j.Slf4j;
+import org.apache.directory.scim.client.rest.FilterBuilder;
 import org.apache.directory.scim.spec.protocol.filter.FilterParseException;
 import org.apache.directory.scim.spec.protocol.search.Filter;
 import org.junit.jupiter.api.Test;
 
-import lombok.extern.slf4j.Slf4j;
+import static org.assertj.core.api.Assertions.assertThat;
 
 @Slf4j
-public class FilterBuilderStringTests {
+public class FilterBuilderStringTest {
 
   @Test
-  public void testEndsWith() throws UnsupportedEncodingException, FilterParseException {
-    String encoded = FilterClient.builder().endsWith("address.streetAddress", "Way").toString();
-     new Filter(decode(encoded));
+  public void testEndsWith() throws FilterParseException {
+    Filter filter = FilterBuilder.create().endsWith("address.streetAddress", "Way").build();
+    Filter expected = new Filter("address.streetAddress EW \"Way\"");
+    assertThat(filter).isEqualTo(expected);
   }
 
   @Test
-  public void testStartsWith()  throws UnsupportedEncodingException, FilterParseException {
-    String encoded = FilterClient.builder().startsWith("address.streetAddress", "133").toString();
-    new Filter(decode(encoded));
+  public void testStartsWith()  throws FilterParseException {
+    Filter filter = FilterBuilder.create().startsWith("address.streetAddress", "133").build();
+    Filter expected = new Filter("address.streetAddress SW \"133\"");
+    assertThat(filter).isEqualTo(expected);
   }
 
   @Test
-  public void testContains()  throws UnsupportedEncodingException, FilterParseException {
-    String encoded = FilterClient.builder().contains("address.streetAddress", "MacDuff").toString();
-    new Filter(decode(encoded));
-  }
-
-  private String decode(String encoded) throws UnsupportedEncodingException {
-
-    log.info(encoded);
-    
-    String decoded = URLDecoder.decode(encoded, "UTF-8").replace("%20", " ");
-    
-    log.info(decoded);
-    
-    return decoded;
+  public void testContains()  throws FilterParseException {
+    Filter filter = FilterBuilder.create().contains("address.streetAddress", "MacDuff").build();
+    Filter expected = new Filter("address.streetAddress CO \"MacDuff\"");
+    assertThat(filter).isEqualTo(expected);
   }
 }
diff --git a/scim-client/src/test/java/org/apache/directory/scim/client/filter/FilterBuilderTest.java b/scim-client/src/test/java/org/apache/directory/scim/client/filter/FilterBuilderTest.java
index 90db3df..0dd3eec 100644
--- a/scim-client/src/test/java/org/apache/directory/scim/client/filter/FilterBuilderTest.java
+++ b/scim-client/src/test/java/org/apache/directory/scim/client/filter/FilterBuilderTest.java
@@ -19,206 +19,191 @@
 
 package org.apache.directory.scim.client.filter;
 
-import static org.junit.jupiter.api.Assertions.assertThrows;
-
-import java.io.UnsupportedEncodingException;
-import java.net.URLDecoder;
-
+import lombok.extern.slf4j.Slf4j;
+import org.apache.directory.scim.client.rest.FilterBuilder;
 import org.apache.directory.scim.spec.protocol.filter.FilterParseException;
 import org.apache.directory.scim.spec.protocol.search.Filter;
+import org.assertj.core.api.Assertions;
 import org.junit.jupiter.api.Test;
 
-import lombok.extern.slf4j.Slf4j;
+import java.beans.XMLEncoder;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 
 @Slf4j
 public class FilterBuilderTest {
 
-//  address.type EQ "work"
-//  address.primary EQ true
-//  address.primary EQ false
-//  address.primary EQ null
-//  address.number EQ 123
-//  address.number EQ 123 OR address.primary EQ null
-//  address.number EQ 123 AND address.primary EQ null
-//  address.number EQ 123 OR NOT(address.primary EQ null)
-//  NOT(address.number EQ 123) OR NOT(address.primary EQ null)
-//  (address.number EQ 123) OR (address.primary EQ null)
-//  ((address.number EQ 123) OR (address.primary EQ null))
-//  address.primary PR
-//  NOT(address.primary PR)
-//  urn:scimscim:Custom+2.0:address EQ "work"
-//  urn:justlongenoughnidxxxxxxxxxxxxxxx:Custom:address EQ "work"
-//  urn:scim:Custom()+,-.:=@;$_!*\nss:address EQ "work"
-//  address.type2_-3 EQ "work"
-//
-//  Invalid:
-//  address.type EQ "work" MAYBE address.type EQ "home"
-//  address.type YZ "work"
-//  address.type EQ work
-//  address..type EQ "work"
-//  .address.type EQ "work"
-//  address[street[apt EQ "100"]]
-//  address[type EQ "work" AND street[apt EQ "100"]]
-//  urn:scim+scim:Custom:address EQ "work"
-//  urn:scim+scim:Custom.address EQ "work"
-//  urn:reallyreallyreallyreallylongnidxx:Custom:address EQ "work"
-//  address.type EQ "work" OR "home"
-//  NOT(address.primary)
-//  address^.type EQ "work"
-//  address,type EQ "work"
-//  address.2type EQ "work"
-
-  
   @Test
-  public void testSimpleAnd() throws UnsupportedEncodingException, FilterParseException {
-  
-    String encoded = FilterClient.builder().equalTo("name.givenName", "Bilbo").and().equalTo("name.familyName", "Baggins").toString();
-  
-    String decoded = decode(encoded);
-    new Filter(decoded); 
+  public void testSimpleAnd() throws FilterParseException {
+    Filter filter = FilterBuilder.create()
+      .equalTo("name.givenName", "Bilbo")
+      .and()
+      .equalTo("name.familyName", "Baggins")
+      .build();
+    assertThat(filter).isEqualTo(new Filter("name.givenName EQ \"Bilbo\" AND name.familyName EQ \"Baggins\""));
   }
   
   @Test
-  public void testSimpleOr() throws UnsupportedEncodingException, FilterParseException {
-  
-    String encoded = FilterClient.builder().equalTo("name.givenName", "Bilbo").or().equalTo("name.familyName", "Baggins").toString();
-  
-    String decoded = decode(encoded);
-    new Filter(decoded); 
+  public void testSimpleOr() throws FilterParseException {
+    Filter filter = FilterBuilder.create()
+      .equalTo("name.givenName", "Bilbo")
+      .or()
+      .equalTo("name.familyName", "Baggins").build();
+
+    assertThat(filter).isEqualTo(new Filter("name.givenName EQ \"Bilbo\" OR name.familyName EQ \"Baggins\""));
   }
   
   @Test
-  public void testAndOrChain() throws UnsupportedEncodingException, FilterParseException {
-  
-    String encoded = FilterClient.builder().equalTo("name.givenName", "Bilbo").or().equalTo("name.givenName", "Frodo").and().equalTo("name.familyName", "Baggins").toString();
-  
-    String decoded = decode(encoded);
-    new Filter(decoded); 
+  public void testAndOrChain() throws FilterParseException, UnsupportedEncodingException {
+
+    Filter filter = FilterBuilder.create()
+      .equalTo("name.givenName", "Bilbo")
+      .or()
+      .equalTo("name.givenName", "Frodo")
+      .and()
+      .equalTo("name.familyName", "Baggins").build();
+
+    decode(filter.getFilter());
+
+    Filter expected = new Filter("(name.givenName EQ \"Bilbo\" OR name.givenName EQ \"Frodo\") AND name.familyName EQ \"Baggins\"");
+
+    assertThat(filter).isEqualTo(expected);
   }
   
   @Test
-  public void testAndOrChainComplex() throws UnsupportedEncodingException, FilterParseException {
-  
-    String encoded = FilterClient.builder().equalTo("name.givenName", "Bilbo").and(FilterClient.builder().equalTo("name.givenName", "Frodo").and().equalTo("name.familyName", "Baggins").filter()).toString();
-  
-    String decoded = decode(encoded);
-    new Filter(decoded); 
+  public void testAndOrChainComplex() throws FilterParseException, UnsupportedEncodingException {
+
+    Filter filter = FilterBuilder.create()
+      .equalTo("name.givenName", "Bilbo")
+      .and(FilterBuilder.create()
+        .equalTo("name.givenName", "Frodo")
+        .and()
+        .equalTo("name.familyName", "Baggins")
+        .filter())
+      .build();
+
+    decode(filter.getFilter());
+
+    Filter expected = new Filter("name.givenName EQ \"Bilbo\" AND (name.givenName EQ \"Frodo\" AND name.familyName EQ \"Baggins\")");
+    assertThat(filter).isEqualTo(expected);
   }
   
   @Test
-  public void testOrAndChainComplex() throws UnsupportedEncodingException, FilterParseException {
-  
-    String encoded = FilterClient.builder().equalTo("name.givenName", "Bilbo").or(FilterClient.builder().equalTo("name.givenName", "Frodo").and().equalTo("name.familyName", "Baggins").filter()).toString();
-  
-    String decoded = decode(encoded);
-    new Filter(decoded); 
+  public void testOrAndChainComplex() throws FilterParseException {
+
+    Filter filter = FilterBuilder.create()
+      .equalTo("name.givenName", "Bilbo")
+      .or(FilterBuilder.create()
+        .equalTo("name.givenName", "Frodo")
+        .and()
+        .equalTo("name.familyName", "Baggins")
+        .filter())
+      .build();
+
+    Filter expected = new Filter("name.givenName EQ \"Bilbo\" OR (name.givenName EQ \"Frodo\" AND name.familyName EQ \"Baggins\")");
+
+    assertThat(filter).isEqualTo(expected);
   }
   
   @Test
-  public void testComplexAnd() throws UnsupportedEncodingException, FilterParseException {
-  
-    FilterClient.Builder b1 = FilterClient.builder().equalTo("name.givenName", "Bilbo").or().equalTo("name.givenName", "Frodo").and().equalTo("name.familyName", "Baggins");
-    FilterClient.Builder b2 = FilterClient.builder().equalTo("address.streetAddress", "Underhill").or().equalTo("address.streetAddress", "Overhill").and().equalTo("address.postalCode", "16803");
+  public void testComplexAnd() throws FilterParseException {
+  
+    FilterBuilder b1 = FilterBuilder.create()
+      .equalTo("name.givenName", "Bilbo")
+      .or()
+      .equalTo("name.givenName", "Frodo")
+      .and()
+      .equalTo("name.familyName", "Baggins");
+
+    FilterBuilder b2 = FilterBuilder.create().equalTo("address.streetAddress", "Underhill")
+      .or()
+      .equalTo("address.streetAddress", "Overhill")
+      .and()
+      .equalTo("address.postalCode", "16803");
     
-    String encoded = FilterClient.builder().and(b1.filter(), b2.filter()).toString();
+    Filter filter = FilterBuilder.create().and(b1.filter(), b2.filter()).build();
+
+    Filter expected = new Filter("((name.givenName EQ \"Bilbo\" OR name.givenName EQ \"Frodo\") AND name.familyName EQ \"Baggins\") AND ((address.streetAddress EQ \"Underhill\" OR address.streetAddress EQ \"Overhill\") AND address.postalCode EQ \"16803\")");
     
-    String decoded = decode(encoded);
-    new Filter(decoded); 
+    assertThat(filter).isEqualTo(expected);
   }
   
   @Test
-  public void testNot() throws UnsupportedEncodingException, FilterParseException {
-    FilterClient.Builder b1 = FilterClient.builder().equalTo("name.givenName", "Bilbo").or().equalTo("name.givenName", "Frodo").and().equalTo("name.familyName", "Baggins");
-
-    String encoded = FilterClient.builder().not(b1.filter()).toString();
-    
-    String decoded = decode(encoded);
-    new Filter(decoded); 
+  public void testNot() throws FilterParseException {
+    FilterBuilder b1 = FilterBuilder.create()
+      .equalTo("name.givenName", "Bilbo")
+      .or()
+      .equalTo("name.givenName", "Frodo")
+      .and()
+      .equalTo("name.familyName", "Baggins");
+
+    Filter filter = FilterBuilder.create().not(b1.filter()).build();
+    Filter expected = new Filter("NOT((name.givenName EQ \"Bilbo\" OR name.givenName EQ \"Frodo\") AND name.familyName EQ \"Baggins\")");
+    assertThat(filter).isEqualTo(expected);
   }
   
   @Test
-  public void testAttributeContains() throws UnsupportedEncodingException, FilterParseException {
+  public void testAttributeContains() throws FilterParseException {
     
-    FilterClient.Builder b1 = FilterClient.builder().equalTo("name.givenName", "Bilbo").or().equalTo("name.givenName", "Frodo").and().equalTo("name.familyName", "Baggins");
-    FilterClient.Builder b2 = FilterClient.builder().attributeHas("address", b1.filter());
+    FilterBuilder b1 = FilterBuilder.create()
+      .equalTo("address.type", "work");
+    FilterBuilder b2 = FilterBuilder.create()
+      .attributeHas("address", b1.filter());
     
-    String encoded = b2.toString();
-    
-    String decoded = decode(encoded);
-    new Filter(decoded); 
+    Filter filter = b2.build();
+    Filter expected = new Filter("address[type EQ \"work\"]");
+
+    assertThat(filter).isEqualTo(expected);
   }
   
   @Test
-  public void testAttributeContainsEmbedded() throws UnsupportedEncodingException, FilterParseException {
+  public void testAttributeContainsEmbedded() throws FilterParseException {
 
-    FilterClient.Builder b1 = FilterClient.builder().equalTo("name.givenName", "Bilbo").or().equalTo("name.givenName", "Frodo").and().equalTo("name.familyName", "Baggins");
-    FilterClient.Builder b2 = FilterClient.builder().attributeHas("address", b1.filter());
-    
-    FilterClient.Builder b3 = FilterClient.builder().attributeHas("address", b2.filter());
-   
-    String encoded = b3.toString();
+    FilterBuilder b1 = FilterBuilder.create()
+      .equalTo("name.givenName", "Bilbo")
+      .or()
+      .equalTo("name.givenName", "Frodo")
+      .and()
+      .equalTo("name.familyName", "Baggins");
+
+    FilterBuilder b2 = FilterBuilder.create().attributeHas("address", b1.filter());
     
-    String decoded = decode(encoded);
+    FilterBuilder b3 = FilterBuilder.create().attributeHas("address", b2.filter());
 
-    assertThrows(FilterParseException.class, () -> new Filter(decoded));
+    // TODO: I'm not sure why this test exists in the Client
+    // This should probably be a parsing test, and maybe the FilterBuilder should be smarter and throw an
+    // exception when this type Filter is created, instead of just when parsed.
+    String filterString = b3.build().toString();
+    assertThrows(FilterParseException.class, () -> new Filter(filterString));
   }
   
   @Test
-  public void testAttributeContainsDeeplyEmbedded() throws UnsupportedEncodingException, FilterParseException {
+  public void testAttributeContainsDeeplyEmbedded() throws FilterParseException {
 
-      FilterClient.Builder b1 = FilterClient.builder().equalTo("name.givenName", "Bilbo").or().equalTo("name.givenName", "Frodo").and().equalTo("name.familyName", "Baggins");
-      FilterClient.Builder b2 = FilterClient.builder().attributeHas("address", b1.filter());
-      FilterClient.Builder b3 = FilterClient.builder().equalTo("name.giveName", "Gandalf").and(b2.filter());
-      FilterClient.Builder b4 = FilterClient.builder().attributeHas("address", b3.filter());
+      FilterBuilder b1 = FilterBuilder.create().equalTo("name.givenName", "Bilbo").or().equalTo("name.givenName", "Frodo").and().equalTo("name.familyName", "Baggins");
+      FilterBuilder b2 = FilterBuilder.create().attributeHas("address", b1.filter());
+      FilterBuilder b3 = FilterBuilder.create().equalTo("name.giveName", "Gandalf").and(b2.filter());
+      FilterBuilder b4 = FilterBuilder.create().attributeHas("address", b3.filter());
 
       String encoded = b4.toString();
 
       String decoded = decode(encoded);
       assertThrows(FilterParseException.class, () -> new Filter(decoded));
   }
-  //@Test
-//  public void testNotSingleArg() throws UnsupportedEncodingException, FilterParseException {
-// 
-//     String encoded = filterBuilder.not(attributeComparisonExpression, LogicalOperator.AND, attributeComparisonExpression2).toString();
-// 
-//     String decoded = decode(encoded);
-//     Filter filter = new Filter(decoded);
-//  }
-//
-//  //@Test
-//  public void testAnd() throws UnsupportedEncodingException, FilterParseException {
-//
-//    String encoded = filterBuilder.and(attributeComparisonExpression, attributeComparisonExpression2).toString();
-//    
-//    String decoded = decode(encoded);
-//    Filter filter = new Filter(decoded);
-//  }
-//
-//  //@Test
-//  public void testOr() throws UnsupportedEncodingException, FilterParseException {
-//
-//    String encoded = filterBuilder.equalTo("addresses.postalCode", "16801")
-//                                  .and()
-//                                  .or(attributeComparisonExpression, attributeComparisonExpression2)
-//                                  .toString();
-//
-//    log.info(encoded);
-//    
-//    String decoded = URLDecoder.decode(encoded, "UTF-8").replace("%20", " ");
-//    
-//    log.info(decoded);
-//    
-//    Filter filter = new Filter(decoded);
-//  }
-  
-  private String decode(String encoded) throws UnsupportedEncodingException {
+
+  private String decode(String encoded) {
 
     log.info(encoded);
-    
-    String decoded = URLDecoder.decode(encoded, "UTF-8").replace("%20", " ");
-    
+
+    String decoded = URLDecoder.decode(encoded, UTF_8).replace("%20", " ");
+
     log.info(decoded);
-    
+
     return decoded;
   }
 
diff --git a/scim-client/src/test/java/org/apache/directory/scim/client/filter/FilterBuilderTestEquals.java b/scim-client/src/test/java/org/apache/directory/scim/client/filter/FilterBuilderTestEquals.java
deleted file mode 100644
index f437554..0000000
--- a/scim-client/src/test/java/org/apache/directory/scim/client/filter/FilterBuilderTestEquals.java
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
-* Licensed to the Apache Software Foundation (ASF) under one
-* or more contributor license agreements.  See the NOTICE file
-* distributed with this work for additional information
-* regarding copyright ownership.  The ASF licenses this file
-* to you under the Apache License, Version 2.0 (the
-* "License"); you may not use this file except in compliance
-* with the License.  You may obtain a copy of the License at
- 
-* http://www.apache.org/licenses/LICENSE-2.0
-
-* Unless required by applicable law or agreed to in writing,
-* software distributed under the License is distributed on an
-* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-* KIND, either express or implied.  See the License for the
-* specific language governing permissions and limitations
-* under the License.
-*/
-
-package org.apache.directory.scim.client.filter;
-
-import java.io.UnsupportedEncodingException;
-import java.net.URLDecoder;
-import java.time.LocalDate;
-import java.time.LocalDateTime;
-import java.util.Date;
-
-import org.apache.directory.scim.spec.protocol.filter.FilterParseException;
-import org.apache.directory.scim.spec.protocol.search.Filter;
-import org.junit.jupiter.api.Test;
-
-import lombok.extern.slf4j.Slf4j;
-
-@Slf4j
-public class FilterBuilderTestEquals {
-  
-  @Test
-  public void testEqualToStringString() throws UnsupportedEncodingException, FilterParseException {
-    String encoded = FilterClient.builder().equalTo("address.streetAddress", "7714 Sassafrass Way").toString();
-    new Filter(decode(encoded));
-  }
-
-  @Test
-  public void testEqualToStringBoolean() throws UnsupportedEncodingException, FilterParseException {
-    String encoded = FilterClient.builder().equalTo("address.active", true).toString();
-    new Filter(decode(encoded));
-  }
-
-  @Test
-  public void testEqualToStringDate() throws UnsupportedEncodingException, FilterParseException {
-    String encoded = FilterClient.builder().equalTo("date.date", new Date()).toString();
-    new Filter(decode(encoded));
-  }
-
-  @Test
-  public void testEqualToStringLocalDate() throws UnsupportedEncodingException, FilterParseException {
-    String encoded = FilterClient.builder().equalTo("date.date", LocalDate.now()).toString();
-    new Filter(decode(encoded));
-  }
-
-  @Test
-  public void testEqualToStringLocalDateTime() throws UnsupportedEncodingException, FilterParseException {
-    String encoded = FilterClient.builder().equalTo("date.date", LocalDateTime.now()).toString();
-    new Filter(decode(encoded));
-  }
-
-  @Test
-  public void testEqualToStringInteger() throws UnsupportedEncodingException, FilterParseException {
-    int i = 10;
-    String encoded = FilterClient.builder().equalTo("int.int", i).toString();
-    new Filter(decode(encoded));
-  }
-
-  @Test
-  public void testEqualToStringLong() throws UnsupportedEncodingException, FilterParseException {
-    long i = 10l;
-    String encoded = FilterClient.builder().equalTo("long.long", i).toString();
-    new Filter(decode(encoded));
-  }
-
-  @Test
-  public void testEqualToStringFloat() throws UnsupportedEncodingException, FilterParseException {
-    float i = 10.2f;
-    String encoded = FilterClient.builder().equalTo("long.long", i).toString();
-    new Filter(decode(encoded));
-  }
-
-  @Test
-  public void testEqualToStringDouble() throws UnsupportedEncodingException, FilterParseException {
-    double i = 10.2;
-    String encoded = FilterClient.builder().equalTo("long.long", i).toString();
-    new Filter(decode(encoded));
-  }
-
-  @Test
-  public void testEqualNull() throws UnsupportedEncodingException, FilterParseException {
-    String encoded = FilterClient.builder().equalNull("null.null").toString();
-    new Filter(decode(encoded));
-  }
-  
-  private String decode(String encoded) throws UnsupportedEncodingException {
-
-    log.info(encoded);
-    
-    String decoded = URLDecoder.decode(encoded, "UTF-8").replace("%20", " ");
-    
-    log.info(decoded);
-    
-    return decoded;
-  }
-}
diff --git a/scim-client/src/test/java/org/apache/directory/scim/client/filter/FilterBuilderTestNotEquals.java b/scim-client/src/test/java/org/apache/directory/scim/client/filter/FilterBuilderTestNotEquals.java
deleted file mode 100644
index c57a37f..0000000
--- a/scim-client/src/test/java/org/apache/directory/scim/client/filter/FilterBuilderTestNotEquals.java
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
-* Licensed to the Apache Software Foundation (ASF) under one
-* or more contributor license agreements.  See the NOTICE file
-* distributed with this work for additional information
-* regarding copyright ownership.  The ASF licenses this file
-* to you under the Apache License, Version 2.0 (the
-* "License"); you may not use this file except in compliance
-* with the License.  You may obtain a copy of the License at
- 
-* http://www.apache.org/licenses/LICENSE-2.0
-
-* Unless required by applicable law or agreed to in writing,
-* software distributed under the License is distributed on an
-* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-* KIND, either express or implied.  See the License for the
-* specific language governing permissions and limitations
-* under the License.
-*/
-
-package org.apache.directory.scim.client.filter;
-
-import java.io.UnsupportedEncodingException;
-import java.net.URLDecoder;
-import java.time.LocalDate;
-import java.time.LocalDateTime;
-import java.util.Date;
-
-import org.apache.directory.scim.spec.protocol.filter.FilterParseException;
-import org.apache.directory.scim.spec.protocol.search.Filter;
-import org.junit.jupiter.api.Test;
-
-import lombok.extern.slf4j.Slf4j;
-
-@Slf4j
-public class FilterBuilderTestNotEquals {
-
-  @Test
-  public void testNotnotEqualStringString() throws UnsupportedEncodingException, FilterParseException {
-    String encoded = FilterClient.builder().notEqual("address.streetAddress", "7714 Sassafrass Way").toString();
-    new Filter(decode(encoded));
-  }
-
-  @Test
-  public void testNotnotEqualStringBoolean() throws UnsupportedEncodingException, FilterParseException {
-    String encoded = FilterClient.builder().notEqual("address.active", true).toString();
-    new Filter(decode(encoded));
-  }
-
-  @Test
-  public void testNotnotEqualStringDate() throws UnsupportedEncodingException, FilterParseException {
-    String encoded = FilterClient.builder().notEqual("date.date", new Date()).toString();
-    new Filter(decode(encoded));
-  }
-
-  @Test
-  public void testNotnotEqualStringLocalDate() throws UnsupportedEncodingException, FilterParseException {
-    String encoded = FilterClient.builder().notEqual("date.date", LocalDate.now()).toString();
-    new Filter(decode(encoded));
-  }
-
-  @Test
-  public void testNotnotEqualStringLocalDateTime() throws UnsupportedEncodingException, FilterParseException {
-    String encoded = FilterClient.builder().notEqual("date.date", LocalDateTime.now()).toString();
-    new Filter(decode(encoded));
-  }
-
-  @Test
-  public void testNotnotEqualStringInteger() throws UnsupportedEncodingException, FilterParseException {
-    int i = 10;
-    String encoded = FilterClient.builder().notEqual("int.int", i).toString();
-    new Filter(decode(encoded));
-  }
-
-  @Test
-  public void testNotnotEqualStringLong() throws UnsupportedEncodingException, FilterParseException {
-    long i = 10l;
-    String encoded = FilterClient.builder().notEqual("long.long", i).toString();
-    new Filter(decode(encoded));
-  }
-
-  @Test
-  public void testNotnotEqualStringFloat() throws UnsupportedEncodingException, FilterParseException {
-    float i = 10.2f;
-    String encoded = FilterClient.builder().notEqual("long.long", i).toString();
-    new Filter(decode(encoded));
-  }
-
-  @Test
-  public void testNotnotEqualStringDouble() throws UnsupportedEncodingException, FilterParseException {
-    double i = 10.2;
-    String encoded = FilterClient.builder().notEqual("long.long", i).toString();
-    new Filter(decode(encoded));
-  }
-
-  @Test
-  public void testNotEqualNull() throws UnsupportedEncodingException, FilterParseException {
-    String encoded = FilterClient.builder().equalNull("null.null").toString();
-    new Filter(decode(encoded));
-  }
-  
-  private String decode(String encoded) throws UnsupportedEncodingException {
-
-    log.info(encoded);
-    
-    String decoded = URLDecoder.decode(encoded, "UTF-8").replace("%20", " ");
-    
-    log.info(decoded);
-    
-    return decoded;
-  }
-}
diff --git a/scim-spec/scim-spec-protocol/src/main/java/org/apache/directory/scim/spec/protocol/filter/ExpressionBuildingListener.java b/scim-spec/scim-spec-protocol/src/main/java/org/apache/directory/scim/spec/protocol/filter/ExpressionBuildingListener.java
index a0cccf8..186b835 100644
--- a/scim-spec/scim-spec-protocol/src/main/java/org/apache/directory/scim/spec/protocol/filter/ExpressionBuildingListener.java
+++ b/scim-spec/scim-spec-protocol/src/main/java/org/apache/directory/scim/spec/protocol/filter/ExpressionBuildingListener.java
@@ -65,6 +65,11 @@ public class ExpressionBuildingListener extends FilterBaseListener {
     AttributeReference attributeReference = new AttributeReference(attributePath);
     String urn = attributeReference.getUrn();
     String parentAttributeName = attributeReference.getAttributeName();
+
+    if (expressionStack.size() == 0) {
+      throw new IllegalStateException("Invalid Expression " + ctx.attributePath);
+    }
+
     FilterExpression attributeExpression = (FilterExpression) expressionStack.pop();
     ValuePathExpression valuePathExpression = new ValuePathExpression(attributeReference, attributeExpression);
 
@@ -169,7 +174,11 @@ public class ExpressionBuildingListener extends FilterBaseListener {
       return false;
     } else {
       try {
-        return Double.parseDouble(jsonValue);
+        if(jsonValue.contains(".")) {
+          return Double.parseDouble(jsonValue);
+        } else {
+          return Integer.parseInt(jsonValue);
+        }
       } catch (NumberFormatException e) {
         LOG.warn("Unable to parse a json number: " + jsonValue);
       }
diff --git a/scim-spec/scim-spec-protocol/src/main/java/org/apache/directory/scim/spec/protocol/search/Filter.java b/scim-spec/scim-spec-protocol/src/main/java/org/apache/directory/scim/spec/protocol/search/Filter.java
index 43a0545..5ab936d 100644
--- a/scim-spec/scim-spec-protocol/src/main/java/org/apache/directory/scim/spec/protocol/search/Filter.java
+++ b/scim-spec/scim-spec-protocol/src/main/java/org/apache/directory/scim/spec/protocol/search/Filter.java
@@ -19,23 +19,23 @@
 
 package org.apache.directory.scim.spec.protocol.search;
 
-import org.antlr.v4.runtime.ANTLRInputStream;
-import org.antlr.v4.runtime.BaseErrorListener;
-import org.antlr.v4.runtime.CommonTokenStream;
-import org.antlr.v4.runtime.RecognitionException;
-import org.antlr.v4.runtime.Recognizer;
+import lombok.AccessLevel;
+import lombok.Data;
+import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
+import org.antlr.v4.runtime.*;
 import org.antlr.v4.runtime.tree.ParseTree;
 import org.antlr.v4.runtime.tree.ParseTreeWalker;
-
 import org.apache.directory.scim.server.filter.FilterLexer;
 import org.apache.directory.scim.server.filter.FilterParser;
 import org.apache.directory.scim.spec.protocol.filter.ExpressionBuildingListener;
 import org.apache.directory.scim.spec.protocol.filter.FilterExpression;
 import org.apache.directory.scim.spec.protocol.filter.FilterParseException;
-import lombok.AccessLevel;
-import lombok.Data;
-import lombok.Setter;
-import lombok.extern.slf4j.Slf4j;
+
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
 
 /**
  * 
@@ -60,7 +60,7 @@ public class Filter {
   public Filter(FilterExpression filterExpression) {
     log.debug("Creating a filter - {}", filterExpression);
     expression = filterExpression;
-    this.filter = filterExpression.toString();
+    this.filter = filterExpression.toFilter();
   }
   
   /**
@@ -99,4 +99,23 @@ public class Filter {
   public String toString() {
     return expression.toFilter();
   }
+
+
+  public String encode() {
+    String filterString = expression.toFilter();
+    return URLEncoder.encode(filterString, UTF_8).replace("+", "%20");
+  }
+
+  public static Filter decode(String encodedExpression) throws FilterParseException {
+    String decoded = URLDecoder.decode(encodedExpression, UTF_8).replace("%20", " ");
+    return new Filter(decoded);
+  }
+
+  public FilterExpression getExpression() {
+    return this.expression;
+  }
+
+  public String getFilter() {
+    return this.filter;
+  }
 }