sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From jso...@apache.org
Subject [sis] branch geoapi-4.0 updated: CQL : add AntLR filter parser
Date Fri, 16 Aug 2019 08:29:38 GMT
This is an automated email from the ASF dual-hosted git repository.

jsorel pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new 7daf0a9  CQL : add AntLR filter parser
7daf0a9 is described below

commit 7daf0a91ab402513f9f53d07b7508f2126c1050b
Author: jsorel <johann.sorel@geomatys.com>
AuthorDate: Thu Feb 21 10:09:39 2019 +0100

    CQL : add AntLR filter parser
---
 core/pom.xml                                       |   1 +
 core/sis-cql/nb-configuration.xml                  |  18 +
 core/sis-cql/pom.xml                               | 181 +++++
 .../main/antlr4/org/apache/sis/internal/cql/CQL.g4 | 280 +++++++
 .../src/main/java/org/apache/sis/cql/CQL.java      | 847 ++++++++++++++++++++
 .../main/java/org/apache/sis/cql/CQLException.java |  38 +
 .../org/apache/sis/cql/FilterToCQLVisitor.java     | 659 ++++++++++++++++
 .../test/java/org/apache/sis/cql/CQLTestSuite.java |  39 +
 .../org/apache/sis/cql/ExpressionReadingTest.java  | 618 +++++++++++++++
 .../org/apache/sis/cql/ExpressionWritingTest.java  | 344 +++++++++
 .../java/org/apache/sis/cql/FilterReadingTest.java | 849 +++++++++++++++++++++
 .../java/org/apache/sis/cql/FilterWritingTest.java | 416 ++++++++++
 .../java/org/apache/sis/cql/TemporalUtilities.java |  34 +
 13 files changed, 4324 insertions(+)

diff --git a/core/pom.xml b/core/pom.xml
index 183699d..7bfca04 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -192,6 +192,7 @@
     <module>sis-referencing</module>
     <module>sis-referencing-by-identifiers</module>
     <module>sis-feature</module>
+    <module>sis-cql</module>
     <module>sis-portrayal</module>
   </modules>
 
diff --git a/core/sis-cql/nb-configuration.xml b/core/sis-cql/nb-configuration.xml
new file mode 100644
index 0000000..8b36389
--- /dev/null
+++ b/core/sis-cql/nb-configuration.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project-shared-configuration>
+    <!--
+This file contains additional configuration written by modules in the NetBeans IDE.
+The configuration is intended to be shared among all the users of project and
+therefore it is assumed to be part of version control checkout.
+Without this configuration present, some functionality in the IDE may be limited or fail altogether.
+-->
+    <properties xmlns="http://www.netbeans.org/ns/maven-properties-data/1">
+        <!--
+Properties that influence various parts of the IDE, especially code formatting and the like. 
+You can copy and paste the single properties, into the pom.xml file and the IDE will pick them up.
+That way multiple projects can share the same settings (useful for formatting rules for example).
+Any value defined here will override the pom.xml file value but is only applicable to the current project.
+-->
+        <netbeans.hint.jdkPlatform>JDK_10</netbeans.hint.jdkPlatform>
+    </properties>
+</project-shared-configuration>
diff --git a/core/sis-cql/pom.xml b/core/sis-cql/pom.xml
new file mode 100644
index 0000000..feec340
--- /dev/null
+++ b/core/sis-cql/pom.xml
@@ -0,0 +1,181 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+  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.
+-->
+
+<project xmlns              = "http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi          = "http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation = "http://maven.apache.org/POM/4.0.0
+                               http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.sis</groupId>
+    <artifactId>core</artifactId>
+    <version>2.0-SNAPSHOT</version>
+  </parent>
+
+
+  <!-- ===========================================================
+           Module Description
+       =========================================================== -->
+  <groupId>org.apache.sis.core</groupId>
+  <artifactId>sis-cql</artifactId>
+  <name>Apache SIS CQL</name>
+  <description>
+    CQL parser.
+  </description>
+
+
+  <!-- ===========================================================
+           Developers and Contributors
+       =========================================================== -->
+  <developers>
+    <developer>
+      <name>Travis L. Pinney</name>
+      <id>tlpinney</id>
+      <email>travis.pinney@gmail.com</email>
+      <roles>
+        <role>developer</role>
+      </roles>
+    </developer>
+    <developer>
+      <name>Martin Desruisseaux</name>
+      <id>desruisseaux</id>
+      <email>desruisseaux@apache.org</email>
+      <organization>Geomatys</organization>
+      <organizationUrl>http://www.geomatys.com</organizationUrl>
+      <timezone>+1</timezone>
+      <roles>
+        <role>developer</role>
+      </roles>
+    </developer>
+  </developers>
+  <contributors>
+    <contributor>
+      <name>Marc le Bihan</name>
+      <roles>
+        <role>developer</role>
+      </roles>
+    </contributor>
+  </contributors>
+
+
+  <!-- ===========================================================
+           Build configuration
+       =========================================================== -->
+  <build>
+    <plugins>
+      <!-- Anticipation for Java 9 -->
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-jar-plugin</artifactId>
+        <configuration>
+          <archive>
+            <manifestEntries>
+              <Automatic-Module-Name>
+                org.apache.sis.cql
+              </Automatic-Module-Name>
+            </manifestEntries>
+          </archive>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.antlr</groupId>
+        <artifactId>antlr4-maven-plugin</artifactId>
+        <version>4.7</version>
+        <executions>
+          <execution>
+            <id>run antlr</id>
+            <phase>generate-sources</phase>
+            <goals>
+              <goal>antlr4</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-checkstyle-plugin</artifactId>
+        <executions>
+          <execution>
+            <goals>
+              <goal>check</goal>
+            </goals>
+            <configuration>
+              <skip>true</skip>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+
+
+  <!-- ===========================================================
+           Dependencies
+       =========================================================== -->
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.sis.core</groupId>
+      <artifactId>sis-feature</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>com.esri.geometry</groupId>
+      <artifactId>esri-geometry-api</artifactId>
+      <optional>true</optional>
+    </dependency>
+    <dependency>
+      <groupId>org.locationtech.jts</groupId>
+      <artifactId>jts-core</artifactId>
+      <optional>true</optional>
+    </dependency>
+    <dependency>
+      <groupId>org.antlr</groupId>
+      <artifactId>antlr4-runtime</artifactId>
+      <version>4.7</version>
+      <scope>compile</scope>
+    </dependency>
+
+    <!-- Test dependencies -->
+    <dependency>
+      <groupId>org.apache.sis.core</groupId>
+      <artifactId>sis-utility</artifactId>
+      <version>${project.version}</version>
+      <type>test-jar</type>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.sis.core</groupId>
+      <artifactId>sis-metadata</artifactId>
+      <version>${project.version}</version>
+      <type>test-jar</type>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.sis.core</groupId>
+      <artifactId>sis-referencing</artifactId>
+      <version>${project.version}</version>
+      <type>test-jar</type>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+</project>
diff --git a/core/sis-cql/src/main/antlr4/org/apache/sis/internal/cql/CQL.g4 b/core/sis-cql/src/main/antlr4/org/apache/sis/internal/cql/CQL.g4
new file mode 100644
index 0000000..3578ce8
--- /dev/null
+++ b/core/sis-cql/src/main/antlr4/org/apache/sis/internal/cql/CQL.g4
@@ -0,0 +1,280 @@
+/*
+ * 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.
+ */
+grammar CQL;
+
+options {
+    language = Java;
+}
+
+//-----------------------------------------------------------------//
+// LEXER
+//-----------------------------------------------------------------//
+
+
+// GLOBAL STUFF ---------------------------------------
+
+COMMA 	: ',' ;
+WS  :   ( ' ' | '\t' | '\r'| '\n' ) -> skip;
+UNARY : '+' | '-' ;
+MULT : '*' | '/' ;
+fragment DIGIT : '0'..'9' ;
+
+// caseinsensitive , possible alternative solution ?
+fragment A: ('a'|'A');
+fragment B: ('b'|'B');
+fragment C: ('c'|'C');
+fragment D: ('d'|'D');
+fragment E: ('e'|'E');
+fragment F: ('f'|'F');
+fragment G: ('g'|'G');
+fragment H: ('h'|'H');
+fragment I: ('i'|'I');
+fragment J: ('j'|'J');
+fragment K: ('k'|'K');
+fragment L: ('l'|'L');
+fragment M: ('m'|'M');
+fragment N: ('n'|'N');
+fragment O: ('o'|'O');
+fragment P: ('p'|'P');
+fragment Q: ('q'|'Q');
+fragment R: ('r'|'R');
+fragment S: ('s'|'S');
+fragment T: ('t'|'T');
+fragment U: ('u'|'U');
+fragment V: ('v'|'V');
+fragment W: ('w'|'W');
+fragment X: ('x'|'X');
+fragment Y: ('y'|'Y');
+fragment Z: ('z'|'Z');
+fragment LETTER : ~('0'..'9' | ' ' | '\t' | '\r'| '\n' | ',' | '-' | '+' | '*' | '/' | '(' | ')' | '=' | '>' | '<');
+
+LPAREN : '(';
+RPAREN : ')';
+
+
+//LITERALS  ----------------------------------------------
+
+TEXT :   '\'' ( ESC_SEQ | ~('\'') )* '\'' ;
+INT : DIGIT+ ;
+
+FLOAT
+    :   ('0'..'9')+ '.' ('0'..'9')* EXPONENT?
+    |   '.' ('0'..'9')+ EXPONENT?
+    |   ('0'..'9')+ EXPONENT
+    ;
+
+
+// FILTERING OPERAND -----------------------------------
+COMPARE
+	: EQUALABOVE
+	| EQUALUNDER
+	| NOTEQUAL
+	| EQUAL
+	| ABOVE
+	| UNDER
+	;
+fragment EQUALABOVE : '>=' ;
+fragment EQUALUNDER : '<=' ;
+fragment NOTEQUAL   : '<>' ;
+fragment EQUAL      : '=' ;
+fragment ABOVE      : '>' ;
+fragment UNDER      : '<' ;
+LIKE    : L I K E;
+ILIKE   : I L I K E;
+
+IS      : I S ;
+NULL    : N U L L ;
+BETWEEN : B E T W E E N;
+IN      : I N;
+
+
+
+// LOGIC ----------------------------------------------
+AND : A N D;
+OR  : O R ;
+NOT : N O T ;
+
+// GEOMETRIC TYPES AND FILTERS ------------------------
+POINT               : P O I N T ;
+LINESTRING          : L I N E S T R I N G ;
+POLYGON             : P O L Y G O N ;
+MPOINT              : M U L T I P O I N T ;
+MLINESTRING         : M U L T I L I N E S T R I N G ;
+MPOLYGON            : M U L T I P O L Y G O N ;
+GEOMETRYCOLLECTION  : G E O M E T R Y C O L L E C T I O N ;
+ENVELOPE            : E N V E L O P E ;
+EMPTY               : E M P T Y ;
+
+BBOX        : B B O X ;
+BEYOND      : B E Y O N D ;
+CONTAINS    : C O N T A I N S ;
+CROSSES     : C R O S S E S;
+DISJOINT    : D I S J O I N T ;
+DWITHIN     : D W I T H I N ;
+EQUALS      : E Q U A L S ;
+INTERSECTS  : I N T E R S E C T S;
+OVERLAPS    : O V E R L A P S;
+TOUCHES     : T O U C H E S;
+WITHIN      : W I T H I N ;
+
+// TEMPORAL TYPES AND FILTERS
+
+DATE : DIGIT DIGIT DIGIT DIGIT '-' DIGIT DIGIT '-' DIGIT DIGIT 'T' DIGIT DIGIT ':' DIGIT DIGIT ':' DIGIT DIGIT ('.' DIGIT+)? 'Z';
+DURATION_P : P (INT 'Y')? (INT 'M')? (INT 'D')? (INT 'H')? (INT 'M')? (INT 'S')?;
+DURATION_T : T (INT 'H')? (INT 'M')? (INT 'S')?;
+
+AFTER		: A F T E R ;
+ANYINTERACTS	: A N Y I N T E R A C T S ;
+BEFORE		: B E F O R E ;
+BEGINS		: B E G I N S ;
+BEGUNBY		: B E G U N B Y ;
+DURING		: D U R I N G ;
+ENDEDBY		: E N D E D B Y ;
+ENDS		: E N D S ;
+MEETS		: M E E T S ;
+METBY		: M E T B Y ;
+OVERLAPPEDBY	: O V E R L A P P E D B Y ;
+TCONTAINS	: T C O N T A I N S ;
+TEQUALS		: T E Q U A L S ;
+TOVERLAPS	: T O V E R L A P S ;
+
+
+// PROPERTY NAME -------------------------------------
+PROPERTY_NAME    	:  '"' ( ESC_SEQ | ~('\\'|'"') )* '"'    ;
+NAME   	: LETTER (DIGIT|LETTER)* ;
+
+
+// FRAGMENT -------------------------------------------
+
+fragment EXPONENT : ('e'|'E') ('+'|'-')? ('0'..'9')+ ;
+fragment HEX_DIGIT : ('0'..'9'|'a'..'f'|'A'..'F') ;
+
+fragment
+ESC_SEQ
+    :   '\\' ('b'|'t'|'n'|'f'|'r'|'\"'|'\''|'\\')
+    |   UNICODE_ESC
+    |   OCTAL_ESC
+    ;
+
+fragment
+OCTAL_ESC
+    :   '\\' ('0'..'3') ('0'..'7') ('0'..'7')
+    |   '\\' ('0'..'7') ('0'..'7')
+    |   '\\' ('0'..'7')
+    ;
+
+fragment
+UNICODE_ESC
+    :   '\\' 'u' HEX_DIGIT HEX_DIGIT HEX_DIGIT HEX_DIGIT
+    ;
+
+
+
+
+//-----------------------------------------------------------------//
+// PARSER
+//-----------------------------------------------------------------//
+
+expressionNum : INT | FLOAT ;
+expressionUnary : UNARY? expressionNum ;
+
+coordinate          : expressionUnary expressionUnary ;
+coordinateSerie    : LPAREN coordinate (COMMA coordinate)*  RPAREN ;
+coordinateSeries   : LPAREN coordinateSerie (COMMA coordinateSerie)* RPAREN;
+
+expressionGeometry
+	: POINT ( EMPTY | coordinateSerie )
+	| LINESTRING ( EMPTY | coordinateSerie )
+	| POLYGON ( EMPTY | coordinateSeries )
+	| MPOINT ( EMPTY | coordinateSerie )
+	| MLINESTRING  ( EMPTY | coordinateSeries )
+	| MPOLYGON ( EMPTY | LPAREN coordinateSeries (COMMA coordinateSeries)* RPAREN )
+        | GEOMETRYCOLLECTION ( EMPTY | (LPAREN expressionGeometry (COMMA expressionGeometry)* RPAREN) )
+        | ENVELOPE ( EMPTY | (LPAREN expressionUnary COMMA expressionUnary COMMA expressionUnary COMMA expressionUnary RPAREN) )
+	;
+
+expressionFctParam
+        : expression (COMMA expression)*
+        ;
+
+expressionTerm
+	: TEXT
+	| expressionUnary
+	| PROPERTY_NAME
+	| DATE
+	| DURATION_P
+	| DURATION_T
+	| NAME (LPAREN expressionFctParam? RPAREN)?
+	| expressionGeometry
+	| LPAREN expression RPAREN
+	;
+
+expression : expression MULT expression
+           | expression UNARY expression
+           | expressionTerm
+           ;
+
+filterGeometry
+        : BBOX LPAREN expression COMMA expressionUnary COMMA expressionUnary COMMA expressionUnary COMMA expressionUnary (COMMA TEXT)? RPAREN
+        | BEYOND LPAREN expression COMMA expression COMMA expression COMMA expression RPAREN
+        | CONTAINS LPAREN expression COMMA expression RPAREN
+        | CROSSES LPAREN expression COMMA expression RPAREN
+        | DISJOINT LPAREN expression COMMA expression RPAREN
+        | DWITHIN LPAREN expression COMMA expression COMMA expression COMMA expression RPAREN
+        | EQUALS LPAREN expression COMMA expression RPAREN
+        | INTERSECTS LPAREN expression COMMA expression RPAREN
+        | OVERLAPS LPAREN expression COMMA expression RPAREN
+        | TOUCHES LPAREN expression COMMA expression RPAREN
+        | WITHIN LPAREN expression COMMA expression RPAREN
+        ;
+
+filterTerm 	: expression
+                    (
+                              COMPARE  expression
+                            | NOT? IN LPAREN (expressionFctParam )?  RPAREN
+                            | BETWEEN expression AND expression
+                            | NOT? LIKE expression
+                            | NOT? ILIKE expression
+                            | IS NOT? NULL
+                            | AFTER  expression
+                            | ANYINTERACTS expression
+                            | BEFORE expression
+                            | BEGINS expression
+                            | BEGUNBY expression
+                            | DURING expression
+                            | ENDEDBY expression
+                            | ENDS expression
+                            | MEETS expression
+                            | METBY expression
+                            | OVERLAPPEDBY expression
+                            | TCONTAINS expression
+                            | TEQUALS expression
+                            | TOVERLAPS expression
+                    )
+                | filterGeometry
+                ;
+
+filter : filter (AND filter)+
+       | filter (OR filter )+
+       | LPAREN filter RPAREN
+       | NOT (filterTerm | (LPAREN filter RPAREN) )
+       | filterTerm
+       ;
+
+filterOrExpression : filter | expression ;
+
+
diff --git a/core/sis-cql/src/main/java/org/apache/sis/cql/CQL.java b/core/sis-cql/src/main/java/org/apache/sis/cql/CQL.java
new file mode 100644
index 0000000..8ae0bd0
--- /dev/null
+++ b/core/sis-cql/src/main/java/org/apache/sis/cql/CQL.java
@@ -0,0 +1,847 @@
+/*
+ * 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.sis.cql;
+
+import java.time.temporal.TemporalAccessor;
+import java.util.ArrayList;
+import java.util.List;
+import javax.swing.tree.DefaultMutableTreeNode;
+import org.antlr.v4.runtime.CharStreams;
+import org.antlr.v4.runtime.CodePointCharStream;
+import org.antlr.v4.runtime.CommonTokenStream;
+import org.antlr.v4.runtime.RecognitionException;
+import org.antlr.v4.runtime.TokenStream;
+import org.antlr.v4.runtime.tree.ParseTree;
+import org.antlr.v4.runtime.tree.TerminalNode;
+import org.apache.sis.filter.DefaultFilterFactory;
+import org.apache.sis.internal.cql.CQLLexer;
+import org.apache.sis.internal.cql.CQLParser;
+import static org.apache.sis.internal.cql.CQLParser.*;
+import org.apache.sis.internal.cql.CQLParser.CoordinateContext;
+import org.apache.sis.internal.cql.CQLParser.CoordinateSerieContext;
+import org.apache.sis.internal.cql.CQLParser.CoordinateSeriesContext;
+import org.apache.sis.internal.cql.CQLParser.ExpressionContext;
+import org.apache.sis.internal.cql.CQLParser.ExpressionFctParamContext;
+import org.apache.sis.internal.cql.CQLParser.ExpressionGeometryContext;
+import org.apache.sis.internal.cql.CQLParser.ExpressionNumContext;
+import org.apache.sis.internal.cql.CQLParser.ExpressionTermContext;
+import org.apache.sis.internal.cql.CQLParser.ExpressionUnaryContext;
+import org.apache.sis.internal.cql.CQLParser.FilterContext;
+import org.apache.sis.internal.cql.CQLParser.FilterGeometryContext;
+import org.apache.sis.internal.cql.CQLParser.FilterOrExpressionContext;
+import org.apache.sis.internal.cql.CQLParser.FilterTermContext;
+import org.apache.sis.internal.util.StandardDateFormat;
+import org.locationtech.jts.geom.Coordinate;
+import org.locationtech.jts.geom.CoordinateSequence;
+import org.locationtech.jts.geom.Geometry;
+import org.locationtech.jts.geom.GeometryFactory;
+import org.locationtech.jts.geom.LineString;
+import org.locationtech.jts.geom.LinearRing;
+import org.locationtech.jts.geom.Polygon;
+import org.opengis.filter.And;
+import org.opengis.filter.Filter;
+import org.opengis.filter.FilterFactory2;
+import org.opengis.filter.Or;
+import org.opengis.filter.expression.Expression;
+import org.opengis.filter.expression.PropertyName;
+
+/**
+ *
+ * @author Johann Sorel (Geomatys)
+ * @version 1.0
+ * @since 1.0
+ * @module
+ */
+public final class CQL {
+
+    private static final GeometryFactory GF = new GeometryFactory();
+
+    private CQL() {
+    }
+
+    private static Object compileExpression(String cql) {
+        try {
+            //lexer splits input into tokens
+            final CodePointCharStream input = CharStreams.fromString(cql);
+            final TokenStream tokens = new CommonTokenStream(new CQLLexer(input));
+
+            //parser generates abstract syntax tree
+            final CQLParser parser = new CQLParser(tokens);
+            final ExpressionContext ctx = parser.expression();
+            return ctx;
+
+        } catch (RecognitionException e) {
+            throw new IllegalStateException("Recognition exception is never thrown, only declared.");
+        }
+    }
+
+    private static Object compileFilter(String cql) {
+        try {
+            //lexer splits input into tokens
+            final CodePointCharStream input = CharStreams.fromString(cql);
+            final TokenStream tokens = new CommonTokenStream(new CQLLexer(input));
+
+            //parser generates abstract syntax tree
+            final CQLParser parser = new CQLParser(tokens);
+            final FilterContext retfilter = parser.filter();
+
+            return retfilter;
+
+        } catch (RecognitionException e) {
+            throw new IllegalStateException("Recognition exception is never thrown, only declared.");
+        }
+    }
+
+    private static Object compileFilterOrExpression(String cql) {
+        try {
+            //lexer splits input into tokens
+            final CodePointCharStream input = CharStreams.fromString(cql);
+            final TokenStream tokens = new CommonTokenStream(new CQLLexer(input));
+
+            //parser generates abstract syntax tree
+            final CQLParser parser = new CQLParser(tokens);
+            final FilterOrExpressionContext retfilter = parser.filterOrExpression();
+
+            return retfilter;
+
+        } catch (RecognitionException e) {
+            throw new IllegalStateException("Recognition exception is never thrown, only declared.");
+        }
+    }
+
+    public static ParseTree compile(String cql) {
+        final Object obj = compileFilterOrExpression(cql);
+
+        ParseTree tree = null;
+        if (obj instanceof ParseTree) {
+            tree = (ParseTree) obj;
+        }
+
+        return tree;
+    }
+
+    public static Expression parseExpression(String cql) throws CQLException {
+        return parseExpression(cql, null);
+    }
+
+    public static Expression parseExpression(String cql, FilterFactory2 factory) throws CQLException {
+        final Object obj = compileExpression(cql);
+
+        ParseTree tree = null;
+        Expression result = null;
+        if (obj instanceof ExpressionContext) {
+            tree = (ParseTree) obj;
+            if (factory == null) {
+                factory = new DefaultFilterFactory();
+            }
+            result = convertExpression(tree, factory);
+        }
+
+        return result;
+    }
+
+    public static Filter parseFilter(String cql) throws CQLException {
+        return parseFilter(cql, null);
+    }
+
+    public static Filter parseFilter(String cql, FilterFactory2 factory) throws CQLException {
+        cql = cql.trim();
+
+        //bypass parsing for inclusive filter
+        if (cql.isEmpty() || "*".equals(cql)) {
+            return Filter.INCLUDE;
+        }
+
+        final Object obj = compileFilter(cql);
+
+        ParseTree tree = null;
+        Filter result = null;
+        if (obj instanceof FilterContext) {
+            tree = (FilterContext) obj;
+            if (factory == null) {
+                factory = new DefaultFilterFactory();
+            }
+            result = convertFilter(tree, factory);
+        }
+
+        return result;
+    }
+
+    public static String write(Filter filter) {
+        if (filter == null) {
+            return "";
+        }
+
+        final StringBuilder sb = new StringBuilder();
+        filter.accept(FilterToCQLVisitor.INSTANCE, sb);
+        return sb.toString();
+    }
+
+    public static String write(Expression exp) {
+        if (exp == null) {
+            return "";
+        }
+
+        final StringBuilder sb = new StringBuilder();
+        exp.accept(FilterToCQLVisitor.INSTANCE, sb);
+        return sb.toString();
+    }
+
+    /**
+     * Create a TreeNode for the given tree. method is recursive.
+     */
+    private static DefaultMutableTreeNode explore(ParseTree tree) {
+        final DefaultMutableTreeNode node = new DefaultMutableTreeNode(tree);
+
+        final int nb = tree.getChildCount();
+        for (int i = 0; i < nb; i++) {
+            final DefaultMutableTreeNode n = explore((ParseTree) tree.getChild(i));
+            node.add(n);
+        }
+        return node;
+    }
+
+    /**
+     * Convert the given tree in an Expression.
+     */
+    private static Expression convertExpression(ParseTree tree, FilterFactory2 ff) throws CQLException {
+
+        if (tree instanceof ExpressionContext) {
+            //: expression MULT expression
+            //| expression UNARY expression
+            //| expressionTerm
+            if (tree.getChildCount() == 3) {
+                final String operand = tree.getChild(1).getText();
+                final Expression left = convertExpression((ParseTree) tree.getChild(0), ff);
+                final Expression right = convertExpression((ParseTree) tree.getChild(2), ff);
+                if ("*".equals(operand)) {
+                    return ff.multiply(left, right);
+                } else if ("/".equals(operand)) {
+                    return ff.divide(left, right);
+                } else if ("+".equals(operand)) {
+                    return ff.add(left, right);
+                } else if ("-".equals(operand)) {
+                    return ff.subtract(left, right);
+                }
+            } else {
+                return convertExpression(tree.getChild(0), ff);
+            }
+        } //        else if(tree instanceof ExpressionStringContext){
+        //            //strip start and end '
+        //            final String text = tree.getText();
+        //            return ff.literal(text.substring(1, text.length()-1));
+        //        }
+        else if (tree instanceof ExpressionTermContext) {
+            //: expressionString
+            //| expressionUnary
+            //| PROPERTY_NAME
+            //| DATE
+            //| DURATION_P
+            //| DURATION_T
+            //| NAME (LPAREN expressionFctParam? RPAREN)?
+            //| expressionGeometry
+            //| LPAREN expression RPAREN
+
+            //: TEXT
+            //| expressionUnary
+            //| PROPERTY_NAME
+            //| DATE
+            //| DURATION_P
+            //| DURATION_T
+            //| expressionGeometry
+            final ExpressionTermContext exp = (ExpressionTermContext) tree;
+            if (exp.getChildCount() == 1) {
+                return convertExpression(tree.getChild(0), ff);
+            }
+
+            // LPAREN expression RPAREN
+            if (exp.expression() != null) {
+                return convertExpression(exp.expression(), ff);
+            }
+
+            // NAME (LPAREN expressionFctParam? RPAREN)?
+            if (exp.NAME() != null) {
+                final String name = exp.NAME().getText();
+                final ExpressionFctParamContext prm = exp.expressionFctParam();
+                if (prm == null) {
+                    //handle as property name
+                    return ff.property(name);
+                }
+
+                //handle as a function
+                final List<ExpressionContext> params = prm.expression();
+                final List<Expression> exps = new ArrayList<Expression>();
+                for (int i = 0, n = params.size(); i < n; i++) {
+                    exps.add(convertExpression(params.get(i), ff));
+                }
+                return ff.function(name, exps.toArray(new Expression[exps.size()]));
+            }
+
+        } else if (tree instanceof ExpressionUnaryContext) {
+            //: UNARY? expressionNum ;
+            final ExpressionUnaryContext exp = (ExpressionUnaryContext) tree;
+            return ff.literal(unaryAsNumber(exp));
+
+        } else if (tree instanceof ExpressionNumContext) {
+            //: INT | FLOAT ;
+            return convertExpression(tree.getChild(0), ff);
+        } else if (tree instanceof TerminalNode) {
+            final TerminalNode exp = (TerminalNode) tree;
+            final int type = exp.getSymbol().getType();
+            if (PROPERTY_NAME == type) {
+                //strip start and end "
+                final String text = tree.getText();
+                return ff.property(text.substring(1, text.length() - 1));
+            } else if (NAME == type) {
+                return ff.property(tree.getText());
+            } else if (INT == type) {
+                return ff.literal(Integer.valueOf(tree.getText()));
+            } else if (FLOAT == type) {
+                return ff.literal(Double.valueOf(tree.getText()));
+            } else if (DATE == type) {
+                TemporalAccessor ta = StandardDateFormat.FORMAT.parse(tree.getText());
+                return ff.literal(ta);
+            } else if (DURATION_P == type || DURATION_T == type) {
+                // TODO
+                //return ff.literal(TemporalUtilities.getTimeInMillis(tree.getText()));
+            } else if (TEXT == type) {
+                //strip start and end '
+                String text = tree.getText();
+                text = text.replaceAll("\\\\'", "'");
+                return ff.literal(text.substring(1, text.length() - 1));
+            }
+        } else if (tree instanceof ExpressionGeometryContext) {
+            //: POINT ( EMPTY | coordinateSerie )
+            //| LINESTRING ( EMPTY | coordinateSerie )
+            //| POLYGON ( EMPTY | coordinateSeries )
+            //| MPOINT ( EMPTY | coordinateSerie )
+            //| MLINESTRING  ( EMPTY | coordinateSeries )
+            //| MPOLYGON ( EMPTY | LPAREN coordinateSeries (COMMA coordinateSeries)* RPAREN )
+            //| GEOMETRYCOLLECTION ( EMPTY | (LPAREN expressionGeometry (COMMA expressionGeometry)* RPAREN) )
+            //| ENVELOPE ( EMPTY | (LPAREN expressionUnary COMMA expressionUnary COMMA expressionUnary COMMA expressionUnary RPAREN) )
+            final ExpressionGeometryContext exp = (ExpressionGeometryContext) tree;
+            final int type = ((TerminalNode) exp.getChild(0)).getSymbol().getType();
+
+            if (POINT == type) {
+                final ParseTree st = (ParseTree) tree.getChild(1);
+                final CoordinateSequence cs;
+                if (isEmptyToken(st)) {
+                    cs = GF.getCoordinateSequenceFactory().create(new Coordinate[0]);
+                } else {
+                    cs = parseSequence(st);
+                }
+                final Geometry geom = GF.createPoint(cs);
+                return ff.literal(geom);
+            } else if (LINESTRING == type) {
+                final ParseTree st = (ParseTree) tree.getChild(1);
+                final CoordinateSequence cs;
+                if (isEmptyToken(st)) {
+                    cs = GF.getCoordinateSequenceFactory().create(new Coordinate[0]);
+                } else {
+                    cs = parseSequence(st);
+                }
+                final Geometry geom = GF.createLineString(cs);
+                return ff.literal(geom);
+            } else if (POLYGON == type) {
+                final ParseTree st = (ParseTree) tree.getChild(1);
+                final Geometry geom;
+                if (isEmptyToken(st)) {
+                    geom = GF.createPolygon(GF.createLinearRing(new Coordinate[0]), new LinearRing[0]);
+                } else {
+                    final CoordinateSeriesContext series = (CoordinateSeriesContext) st;
+                    final List<CoordinateSerieContext> subs = series.coordinateSerie();
+                    final LinearRing contour = GF.createLinearRing(parseSequence(subs.get(0)));
+                    final int n = subs.size();
+                    final LinearRing[] holes = new LinearRing[n - 1];
+                    for (int i = 1; i < n; i++) {
+                        holes[i - 1] = GF.createLinearRing(parseSequence(subs.get(i)));
+                    }
+                    geom = GF.createPolygon(contour, holes);
+                }
+                return ff.literal(geom);
+            } else if (MPOINT == type) {
+                final ParseTree st = (ParseTree) tree.getChild(1);
+                final CoordinateSequence cs;
+                if (isEmptyToken(st)) {
+                    cs = GF.getCoordinateSequenceFactory().create(new Coordinate[0]);
+                } else {
+                    cs = parseSequence(st);
+                }
+                final Geometry geom = GF.createMultiPoint(cs);
+                return ff.literal(geom);
+            } else if (MLINESTRING == type) {
+                final ParseTree st = (ParseTree) tree.getChild(1);
+                final Geometry geom;
+                if (isEmptyToken(st)) {
+                    geom = GF.createMultiLineString(new LineString[0]);
+                } else {
+                    final CoordinateSeriesContext series = (CoordinateSeriesContext) st;
+                    final List<CoordinateSerieContext> subs = series.coordinateSerie();
+                    final int n = subs.size();
+                    final LineString[] strings = new LineString[n];
+                    for (int i = 0; i < n; i++) {
+                        strings[i] = GF.createLineString(parseSequence(subs.get(i)));
+                    }
+                    geom = GF.createMultiLineString(strings);
+                }
+                return ff.literal(geom);
+            } else if (MPOLYGON == type) {
+                final ParseTree st = (ParseTree) tree.getChild(1);
+                final Geometry geom;
+                if (isEmptyToken(st)) {
+                    geom = GF.createMultiPolygon(new Polygon[0]);
+                } else {
+                    final List<CoordinateSeriesContext> eles = exp.coordinateSeries();
+                    final int n = eles.size();
+                    final Polygon[] polygons = new Polygon[n];
+                    for (int i = 0; i < n; i++) {
+                        final CoordinateSeriesContext polyTree = eles.get(i);
+                        final List<CoordinateSerieContext> subs = polyTree.coordinateSerie();
+                        final LinearRing contour = GF.createLinearRing(parseSequence(subs.get(0)));
+                        final int hn = subs.size();
+                        final LinearRing[] holes = new LinearRing[hn - 1];
+                        for (int j = 1; j < hn; j++) {
+                            holes[j - 1] = GF.createLinearRing(parseSequence(subs.get(j)));
+                        }
+                        final Polygon poly = GF.createPolygon(contour, holes);
+                        polygons[i] = poly;
+                    }
+                    geom = GF.createMultiPolygon(polygons);
+                }
+                return ff.literal(geom);
+            } else if (GEOMETRYCOLLECTION == type) {
+                final ParseTree st = (ParseTree) tree.getChild(1);
+                final Geometry geom;
+                if (isEmptyToken(st)) {
+                    geom = GF.createGeometryCollection(new Geometry[0]);
+                } else {
+                    final List<ExpressionGeometryContext> eles = exp.expressionGeometry();
+                    final int n = eles.size();
+                    final Geometry[] subs = new Geometry[n];
+                    for (int i = 0; i < n; i++) {
+                        final ParseTree subTree = eles.get(i);
+                        final Geometry sub = (Geometry) convertExpression(subTree, ff).evaluate(null);
+                        subs[i] = sub;
+                    }
+                    geom = GF.createGeometryCollection(subs);
+                }
+                return ff.literal(geom);
+            } else if (ENVELOPE == type) {
+                final ParseTree st = (ParseTree) tree.getChild(1);
+                final Geometry geom;
+                if (isEmptyToken(st)) {
+                    geom = GF.createPolygon(GF.createLinearRing(new Coordinate[0]), new LinearRing[0]);
+                } else {
+                    final List<ExpressionUnaryContext> unaries = exp.expressionUnary();
+                    final double west = unaryAsNumber(unaries.get(0)).doubleValue();
+                    final double east = unaryAsNumber(unaries.get(1)).doubleValue();
+                    final double north = unaryAsNumber(unaries.get(2)).doubleValue();
+                    final double south = unaryAsNumber(unaries.get(3)).doubleValue();
+                    final LinearRing contour = GF.createLinearRing(new Coordinate[]{
+                        new Coordinate(west, north),
+                        new Coordinate(east, north),
+                        new Coordinate(east, south),
+                        new Coordinate(west, south),
+                        new Coordinate(west, north)
+                    });
+                    geom = GF.createPolygon(contour, new LinearRing[0]);
+                }
+                return ff.literal(geom);
+            }
+
+            return convertExpression(tree.getChild(0), ff);
+        }
+
+        throw new CQLException("Unreconized expression : type=" + tree.getText());
+    }
+
+    private static boolean isEmptyToken(ParseTree tree) {
+        return tree instanceof TerminalNode && ((TerminalNode) tree).getSymbol().getType() == EMPTY;
+    }
+
+    private static final Number unaryAsNumber(ExpressionUnaryContext tree) {
+        //: UNARY? expressionNum ;
+        final ExpressionUnaryContext exp = (ExpressionUnaryContext) tree;
+        final boolean negate = (exp.UNARY() != null && exp.UNARY().getSymbol().getText().equals("-"));
+        final ExpressionNumContext num = exp.expressionNum();
+        if (num.INT() != null) {
+            int val = Integer.valueOf(num.INT().getText());
+            return negate ? -val : val;
+        } else {
+            double val = Double.valueOf(num.FLOAT().getText());
+            return negate ? -val : val;
+        }
+    }
+
+    private static CoordinateSequence parseSequence(ParseTree tree) {
+        final CoordinateSerieContext exp = (CoordinateSerieContext) tree;
+        final List<CoordinateContext> lst = exp.coordinate();
+        final int size = lst.size();
+        final Coordinate[] coords = new Coordinate[size];
+        for (int i = 0; i < size; i++) {
+            final CoordinateContext cc = lst.get(i);
+            coords[i] = new Coordinate(
+                    unaryAsNumber(cc.expressionUnary(0)).doubleValue(),
+                    unaryAsNumber(cc.expressionUnary(1)).doubleValue());
+        }
+        return GF.getCoordinateSequenceFactory().create(coords);
+    }
+
+    /**
+     * Convert the given tree in a Filter.
+     */
+    private static Filter convertFilter(ParseTree tree, FilterFactory2 ff) throws CQLException {
+
+        if (tree instanceof FilterContext) {
+            //: filter (AND filter)+
+            //| filter (OR filter)+
+            //| LPAREN filter RPAREN
+            //| NOT filterTerm
+            //| filterTerm
+
+            final FilterContext exp = (FilterContext) tree;
+
+            //| filterTerm
+            if (exp.getChildCount() == 1) {
+                return convertFilter(tree.getChild(0), ff);
+            } else if (exp.NOT() != null) {
+                //| NOT (filterTerm | ( LPAREN filter RPAREN ))
+                if (exp.filterTerm() != null) {
+                    return ff.not(convertFilter(exp.filterTerm(), ff));
+                } else {
+                    return ff.not(convertFilter(exp.filter(0), ff));
+                }
+
+            } else if (!exp.AND().isEmpty()) {
+                //: filter (AND filter)+
+                final List<Filter> subs = new ArrayList<Filter>();
+                for (FilterContext f : exp.filter()) {
+                    final Filter sub = convertFilter(f, ff);
+                    if (sub instanceof And) {
+                        subs.addAll(((And) sub).getChildren());
+                    } else {
+                        subs.add(sub);
+                    }
+                }
+                return ff.and(subs);
+            } else if (!exp.OR().isEmpty()) {
+                //| filter (OR filter)+
+                final List<Filter> subs = new ArrayList<Filter>();
+                for (FilterContext f : exp.filter()) {
+                    final Filter sub = convertFilter(f, ff);
+                    if (sub instanceof Or) {
+                        subs.addAll(((Or) sub).getChildren());
+                    } else {
+                        subs.add(sub);
+                    }
+                }
+                return ff.or(subs);
+            } else if (exp.LPAREN() != null) {
+                //| LPAREN filter RPAREN
+                return convertFilter(exp.filter(0), ff);
+            }
+
+        } else if (tree instanceof FilterTermContext) {
+            //: expression
+            //    (
+            //              COMPARE  expression
+            //            | NOT? IN LPAREN (expressionFctParam )?  RPAREN
+            //            | BETWEEN expression AND expression
+            //            | NOT? LIKE expression
+            //            | NOT? ILIKE expression
+            //            | IS NOT? NULL
+            //            | AFTER expression
+            //            | ANYINTERACTS expression
+            //            | BEFORE expression
+            //            | BEGINS expression
+            //            | BEGUNBY expression
+            //            | DURING expression
+            //            | ENDEDBY expression
+            //            | ENDS expression
+            //            | MEETS expression
+            //            | METBY expression
+            //            | OVERLAPPEDBY expression
+            //            | TCONTAINS expression
+            //            | TEQUALS expression
+            //            | TOVERLAPS expression
+            //    )
+            //| filterGeometry
+
+            final FilterTermContext exp = (FilterTermContext) tree;
+            final List<ExpressionContext> exps = exp.expression();
+
+            if (exp.COMPARE() != null) {
+                // expression COMPARE expression
+                final String text = exp.COMPARE().getText();
+                final Expression left = convertExpression(exps.get(0), ff);
+                final Expression right = convertExpression(exps.get(1), ff);
+
+                if ("=".equals(text)) {
+                    return ff.equals(left, right);
+                } else if ("<>".equals(text)) {
+                    return ff.notEqual(left, right);
+                } else if (">".equals(text)) {
+                    return ff.greater(left, right);
+                } else if ("<".equals(text)) {
+                    return ff.less(left, right);
+                } else if (">=".equals(text)) {
+                    return ff.greaterOrEqual(left, right);
+                } else if ("<=".equals(text)) {
+                    return ff.lessOrEqual(left, right);
+                } else if ("<=".equals(text)) {
+                    return ff.lessOrEqual(left, right);
+                }
+            } else if (exp.IN() != null) {
+                // expression NOT? IN LPAREN (expressionFctParam )?  RPAREN
+                final Expression val = convertExpression(exps.get(0), ff);
+                final ExpressionFctParamContext prm = exp.expressionFctParam();
+                final List<ExpressionContext> params = prm.expression();
+                final List<Expression> subexps = new ArrayList<Expression>();
+                for (int i = 0, n = params.size(); i < n; i++) {
+                    subexps.add(convertExpression(params.get(i), ff));
+                }
+
+                final int size = subexps.size();
+                final Filter selection;
+                if (size == 0) {
+                    selection = Filter.EXCLUDE;
+                } else if (size == 1) {
+                    selection = ff.equals(val, subexps.get(0));
+                } else {
+                    final List<Filter> filters = new ArrayList<Filter>();
+                    for (Expression e : subexps) {
+                        filters.add(ff.equals(val, e));
+                    }
+                    selection = ff.or(filters);
+                }
+
+                if (exp.NOT() != null) {
+                    return ff.not(selection);
+                } else {
+                    return selection;
+                }
+            } else if (exp.BETWEEN() != null) {
+                // expression BETWEEN expression AND expression
+                final Expression exp1 = convertExpression(exps.get(0), ff);
+                final Expression exp2 = convertExpression(exps.get(1), ff);
+                final Expression exp3 = convertExpression(exps.get(2), ff);
+                return ff.between(exp1, exp2, exp3);
+
+            } else if (exp.LIKE() != null) {
+                // expression NOT? LIKE expression
+                final Expression left = convertExpression(exps.get(0), ff);
+                final Expression right = convertExpression(exps.get(1), ff);
+                if (exp.NOT() != null) {
+                    return ff.not(ff.like(left, right.evaluate(null, String.class), "%", "_", "\\", true));
+                } else {
+                    return ff.like(left, right.evaluate(null, String.class), "%", "_", "\\", true);
+                }
+
+            } else if (exp.ILIKE() != null) {
+                // expression NOT? LIKE expression
+                final Expression left = convertExpression(exps.get(0), ff);
+                final Expression right = convertExpression(exps.get(1), ff);
+                if (exp.NOT() != null) {
+                    return ff.not(ff.like(left, right.evaluate(null, String.class), "%", "_", "\\", false));
+                } else {
+                    return ff.like(left, right.evaluate(null, String.class), "%", "_", "\\", false);
+                }
+
+            } else if (exp.IS() != null) {
+                // expression IS NOT? NULL
+                final Expression exp1 = convertExpression(exps.get(0), ff);
+                if (exp.NOT() != null) {
+                    return ff.not(ff.isNull(exp1));
+                } else {
+                    return ff.isNull(exp1);
+                }
+
+            } else if (exp.AFTER() != null) {
+                // expression AFTER expression
+                final Expression left = convertExpression(exps.get(0), ff);
+                final Expression right = convertExpression(exps.get(1), ff);
+                return ff.after(left, right);
+
+            } else if (exp.ANYINTERACTS() != null) {
+                // expression ANYINTERACTS expression
+                final Expression left = convertExpression(exps.get(0), ff);
+                final Expression right = convertExpression(exps.get(1), ff);
+                return ff.anyInteracts(left, right);
+
+            } else if (exp.BEFORE() != null) {
+                // expression BEFORE expression
+                final Expression left = convertExpression(exps.get(0), ff);
+                final Expression right = convertExpression(exps.get(1), ff);
+                return ff.before(left, right);
+
+            } else if (exp.BEGINS() != null) {
+                // expression BEGINS expression
+                final Expression left = convertExpression(exps.get(0), ff);
+                final Expression right = convertExpression(exps.get(1), ff);
+                return ff.begins(left, right);
+
+            } else if (exp.BEGUNBY() != null) {
+                // expression BEGUNBY expression
+                final Expression left = convertExpression(exps.get(0), ff);
+                final Expression right = convertExpression(exps.get(1), ff);
+                return ff.begunBy(left, right);
+
+            } else if (exp.DURING() != null) {
+                // expression DURING expression
+                final Expression left = convertExpression(exps.get(0), ff);
+                final Expression right = convertExpression(exps.get(1), ff);
+                return ff.during(left, right);
+
+            } else if (exp.ENDEDBY() != null) {
+                // expression ENDEDBY expression
+                final Expression left = convertExpression(exps.get(0), ff);
+                final Expression right = convertExpression(exps.get(1), ff);
+                return ff.endedBy(left, right);
+
+            } else if (exp.ENDS() != null) {
+                // expression ENDS expression
+                final Expression left = convertExpression(exps.get(0), ff);
+                final Expression right = convertExpression(exps.get(1), ff);
+                return ff.ends(left, right);
+
+            } else if (exp.MEETS() != null) {
+                // expression MEETS expression
+                final Expression left = convertExpression(exps.get(0), ff);
+                final Expression right = convertExpression(exps.get(1), ff);
+                return ff.meets(left, right);
+
+            } else if (exp.METBY() != null) {
+                // expression METBY expression
+                final Expression left = convertExpression(exps.get(0), ff);
+                final Expression right = convertExpression(exps.get(1), ff);
+                return ff.metBy(left, right);
+
+            } else if (exp.OVERLAPPEDBY() != null) {
+                // expression OVERLAPPEDBY expression
+                final Expression left = convertExpression(exps.get(0), ff);
+                final Expression right = convertExpression(exps.get(1), ff);
+                return ff.overlappedBy(left, right);
+
+            } else if (exp.TCONTAINS() != null) {
+                // expression TCONTAINS expression
+                final Expression left = convertExpression(exps.get(0), ff);
+                final Expression right = convertExpression(exps.get(1), ff);
+                return ff.tcontains(left, right);
+
+            } else if (exp.TEQUALS() != null) {
+                // expression TEQUALS expression
+                final Expression left = convertExpression(exps.get(0), ff);
+                final Expression right = convertExpression(exps.get(1), ff);
+                return ff.tequals(left, right);
+
+            } else if (exp.TOVERLAPS() != null) {
+                // expression TOVERLAPS expression
+                final Expression left = convertExpression(exps.get(0), ff);
+                final Expression right = convertExpression(exps.get(1), ff);
+                return ff.toverlaps(left, right);
+
+            } else if (exp.filterGeometry() != null) {
+                //expression filterGeometry
+                return convertFilter(exp.filterGeometry(), ff);
+            }
+
+        } else if (tree instanceof FilterGeometryContext) {
+            //: BBOX LPAREN (PROPERTY_NAME|NAME) COMMA expressionUnary COMMA expressionUnary COMMA expressionUnary COMMA expressionUnary (COMMA TEXT)? RPAREN
+            //| BEYOND LPAREN expression COMMA expression COMMA expression COMMA expression RPAREN
+            //| CONTAINS LPAREN expression COMMA expression RPAREN
+            //| CROSSES LPAREN expression COMMA expression RPAREN
+            //| DISJOINT LPAREN expression COMMA expression RPAREN
+            //| DWITHIN LPAREN expression COMMA expression COMMA expression COMMA expression RPAREN
+            //| EQUALS LPAREN expression COMMA expression RPAREN
+            //| INTERSECTS LPAREN expression COMMA expression RPAREN
+            //| OVERLAPS LPAREN expression COMMA expression RPAREN
+            //| TOUCHES LPAREN expression COMMA expression RPAREN
+            //| WITHIN LPAREN expression COMMA expression RPAREN
+
+            final FilterGeometryContext exp = (FilterGeometryContext) tree;
+            final List<ExpressionContext> exps = exp.expression();
+
+            if (exp.BBOX() != null) {
+                final Expression prop = convertExpression(exps.get(0), ff);
+                final double v1 = unaryAsNumber(exp.expressionUnary(0)).doubleValue();
+                final double v2 = unaryAsNumber(exp.expressionUnary(1)).doubleValue();
+                final double v3 = unaryAsNumber(exp.expressionUnary(2)).doubleValue();
+                final double v4 = unaryAsNumber(exp.expressionUnary(3)).doubleValue();
+                String crs = null;
+                if (exp.TEXT() != null) {
+                    crs = convertExpression(exp.TEXT(), ff).evaluate(null, String.class);
+                }
+                return ff.bbox(prop, v1, v2, v3, v4, crs);
+            } else if (exp.BEYOND() != null) {
+                final Expression exp1 = convertExpression(exps.get(0), ff);
+                final Expression exp2 = convertExpression(exps.get(1), ff);
+                final double distance = convertExpression(exps.get(2), ff).evaluate(null, Double.class);
+                final Expression unitExp = convertExpression(exps.get(3), ff);
+                final String unit = (unitExp instanceof PropertyName)
+                        ? ((PropertyName) unitExp).getPropertyName()
+                        : unitExp.evaluate(null, String.class);
+                return ff.beyond(exp1, exp2, distance, unit);
+            } else if (exp.CONTAINS() != null) {
+                final Expression exp1 = convertExpression(exps.get(0), ff);
+                final Expression exp2 = convertExpression(exps.get(1), ff);
+                return ff.contains(exp1, exp2);
+            } else if (exp.CROSSES() != null) {
+                final Expression exp1 = convertExpression(exps.get(0), ff);
+                final Expression exp2 = convertExpression(exps.get(1), ff);
+                return ff.crosses(exp1, exp2);
+            } else if (exp.DISJOINT() != null) {
+                final Expression exp1 = convertExpression(exps.get(0), ff);
+                final Expression exp2 = convertExpression(exps.get(1), ff);
+                return ff.disjoint(exp1, exp2);
+            } else if (exp.DWITHIN() != null) {
+                final Expression exp1 = convertExpression(exps.get(0), ff);
+                final Expression exp2 = convertExpression(exps.get(1), ff);
+                final double distance = convertExpression(exps.get(2), ff).evaluate(null, Double.class);
+                final Expression unitExp = convertExpression(exps.get(3), ff);
+                final String unit = (unitExp instanceof PropertyName)
+                        ? ((PropertyName) unitExp).getPropertyName()
+                        : unitExp.evaluate(null, String.class);
+                return ff.dwithin(exp1, exp2, distance, unit);
+            } else if (exp.EQUALS() != null) {
+                final Expression exp1 = convertExpression(exps.get(0), ff);
+                final Expression exp2 = convertExpression(exps.get(1), ff);
+                return ff.equal(exp1, exp2);
+            } else if (exp.INTERSECTS() != null) {
+                final Expression exp1 = convertExpression(exps.get(0), ff);
+                final Expression exp2 = convertExpression(exps.get(1), ff);
+                return ff.intersects(exp1, exp2);
+            } else if (exp.OVERLAPS() != null) {
+                final Expression exp1 = convertExpression(exps.get(0), ff);
+                final Expression exp2 = convertExpression(exps.get(1), ff);
+                return ff.overlaps(exp1, exp2);
+            } else if (exp.TOUCHES() != null) {
+                final Expression exp1 = convertExpression(exps.get(0), ff);
+                final Expression exp2 = convertExpression(exps.get(1), ff);
+                return ff.touches(exp1, exp2);
+            } else if (exp.WITHIN() != null) {
+                final Expression exp1 = convertExpression(exps.get(0), ff);
+                final Expression exp2 = convertExpression(exps.get(1), ff);
+                return ff.within(exp1, exp2);
+            }
+
+        }
+
+        throw new CQLException("Unreconized filter : type=" + tree.getText());
+
+    }
+
+}
diff --git a/core/sis-cql/src/main/java/org/apache/sis/cql/CQLException.java b/core/sis-cql/src/main/java/org/apache/sis/cql/CQLException.java
new file mode 100644
index 0000000..ab49755
--- /dev/null
+++ b/core/sis-cql/src/main/java/org/apache/sis/cql/CQLException.java
@@ -0,0 +1,38 @@
+/*
+ * 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.sis.cql;
+
+
+/**
+ * CQL exception.
+ *
+ * @author  Johann Sorel (Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+public final class CQLException extends Exception{
+
+    public CQLException(String message) {
+        super(message);
+    }
+
+    public CQLException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+}
diff --git a/core/sis-cql/src/main/java/org/apache/sis/cql/FilterToCQLVisitor.java b/core/sis-cql/src/main/java/org/apache/sis/cql/FilterToCQLVisitor.java
new file mode 100644
index 0000000..f1ca38b
--- /dev/null
+++ b/core/sis-cql/src/main/java/org/apache/sis/cql/FilterToCQLVisitor.java
@@ -0,0 +1,659 @@
+/*
+ * 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.sis.cql;
+
+import java.util.Date;
+import java.util.List;
+import java.util.SimpleTimeZone;
+import java.util.TimeZone;
+import java.util.regex.Pattern;
+import org.apache.sis.internal.util.StandardDateFormat;
+import org.locationtech.jts.geom.Geometry;
+import org.locationtech.jts.io.WKTWriter;
+import org.opengis.filter.And;
+import org.opengis.filter.ExcludeFilter;
+import org.opengis.filter.Filter;
+import org.opengis.filter.FilterVisitor;
+import org.opengis.filter.Id;
+import org.opengis.filter.IncludeFilter;
+import org.opengis.filter.Not;
+import org.opengis.filter.Or;
+import org.opengis.filter.PropertyIsBetween;
+import org.opengis.filter.PropertyIsEqualTo;
+import org.opengis.filter.PropertyIsGreaterThan;
+import org.opengis.filter.PropertyIsGreaterThanOrEqualTo;
+import org.opengis.filter.PropertyIsLessThan;
+import org.opengis.filter.PropertyIsLessThanOrEqualTo;
+import org.opengis.filter.PropertyIsLike;
+import org.opengis.filter.PropertyIsNil;
+import org.opengis.filter.PropertyIsNotEqualTo;
+import org.opengis.filter.PropertyIsNull;
+import org.opengis.filter.expression.Add;
+import org.opengis.filter.expression.Divide;
+import org.opengis.filter.expression.Expression;
+import org.opengis.filter.expression.ExpressionVisitor;
+import org.opengis.filter.expression.Function;
+import org.opengis.filter.expression.Literal;
+import org.opengis.filter.expression.Multiply;
+import org.opengis.filter.expression.NilExpression;
+import org.opengis.filter.expression.PropertyName;
+import org.opengis.filter.expression.Subtract;
+import org.opengis.filter.spatial.BBOX;
+import org.opengis.filter.spatial.Beyond;
+import org.opengis.filter.spatial.Contains;
+import org.opengis.filter.spatial.Crosses;
+import org.opengis.filter.spatial.DWithin;
+import org.opengis.filter.spatial.Disjoint;
+import org.opengis.filter.spatial.Equals;
+import org.opengis.filter.spatial.Intersects;
+import org.opengis.filter.spatial.Overlaps;
+import org.opengis.filter.spatial.Touches;
+import org.opengis.filter.spatial.Within;
+import org.opengis.filter.temporal.After;
+import org.opengis.filter.temporal.AnyInteracts;
+import org.opengis.filter.temporal.Before;
+import org.opengis.filter.temporal.Begins;
+import org.opengis.filter.temporal.BegunBy;
+import org.opengis.filter.temporal.During;
+import org.opengis.filter.temporal.EndedBy;
+import org.opengis.filter.temporal.Ends;
+import org.opengis.filter.temporal.Meets;
+import org.opengis.filter.temporal.MetBy;
+import org.opengis.filter.temporal.OverlappedBy;
+import org.opengis.filter.temporal.TContains;
+import org.opengis.filter.temporal.TEquals;
+import org.opengis.filter.temporal.TOverlaps;
+
+/**
+ * Visitor to convert a Filter in CQL.<br>
+ * Returned object is a StringBuilder containing the CQL text.
+ *
+ * @author Johann Sorel (Geomatys)
+ * @version 1.0
+ * @since 1.0
+ * @module
+ */
+final class FilterToCQLVisitor implements FilterVisitor, ExpressionVisitor {
+
+    public static final FilterToCQLVisitor INSTANCE = new FilterToCQLVisitor();
+    private static final TimeZone TZ = new SimpleTimeZone(0, "Out Timezone");
+
+    /**
+     * Pattern to check for property name to escape against regExp
+     */
+    private final Pattern patternPropertyName = Pattern.compile("[,+\\-/*\\t\\n\\r\\d\\s]");
+
+    private FilterToCQLVisitor() {
+    }
+
+    private static StringBuilder toStringBuilder(final Object o) {
+        if (o instanceof StringBuilder) {
+            return (StringBuilder) o;
+        }
+        return new StringBuilder();
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    // FILTER //////////////////////////////////////////////////////////////////
+    ////////////////////////////////////////////////////////////////////////////
+    @Override
+    public Object visitNullFilter(final Object o) {
+        throw new UnsupportedOperationException("Null filter not supported in CQL.");
+    }
+
+    @Override
+    public Object visit(final ExcludeFilter filter, final Object o) {
+        final StringBuilder sb = toStringBuilder(o);
+        sb.append("1=0");
+        return sb;
+    }
+
+    @Override
+    public Object visit(final IncludeFilter filter, final Object o) {
+        final StringBuilder sb = toStringBuilder(o);
+        sb.append("1=1");
+        return sb;
+    }
+
+    @Override
+    public Object visit(final And filter, final Object o) {
+        final StringBuilder sb = toStringBuilder(o);
+        final List<Filter> filters = filter.getChildren();
+        if (filters != null && !filters.isEmpty()) {
+            final int size = filters.size();
+            sb.append('(');
+            for (int i = 0, n = size - 1; i < n; i++) {
+                filters.get(i).accept(this, sb);
+                sb.append(" AND ");
+            }
+            filters.get(size - 1).accept(this, sb);
+            sb.append(')');
+        }
+        return sb;
+    }
+
+    @Override
+    public Object visit(final Or filter, final Object o) {
+        final StringBuilder sb = toStringBuilder(o);
+        final List<Filter> filters = filter.getChildren();
+        if (filters != null && !filters.isEmpty()) {
+            final int size = filters.size();
+            sb.append('(');
+            for (int i = 0, n = size - 1; i < n; i++) {
+                filters.get(i).accept(this, sb);
+                sb.append(" OR ");
+            }
+            filters.get(size - 1).accept(this, sb);
+            sb.append(')');
+        }
+        return sb;
+    }
+
+    @Override
+    public Object visit(final Id filter, final Object o) {
+        throw new UnsupportedOperationException("ID filter not supported in CQL.");
+    }
+
+    @Override
+    public Object visit(final Not filter, final Object o) {
+        final StringBuilder sb = toStringBuilder(o);
+        sb.append("NOT ");
+        filter.getFilter().accept(this, sb);
+        return sb;
+    }
+
+    @Override
+    public Object visit(final PropertyIsBetween filter, final Object o) {
+        final StringBuilder sb = toStringBuilder(o);
+        filter.getExpression().accept(this, sb);
+        sb.append(" BETWEEN ");
+        filter.getLowerBoundary().accept(this, sb);
+        sb.append(" AND ");
+        filter.getUpperBoundary().accept(this, sb);
+        return sb;
+    }
+
+    @Override
+    public Object visit(final PropertyIsEqualTo filter, final Object o) {
+        final StringBuilder sb = toStringBuilder(o);
+        filter.getExpression1().accept(this, sb);
+        sb.append(" = ");
+        filter.getExpression2().accept(this, sb);
+        return sb;
+    }
+
+    @Override
+    public Object visit(final PropertyIsNotEqualTo filter, final Object o) {
+        final StringBuilder sb = toStringBuilder(o);
+        filter.getExpression1().accept(this, sb);
+        sb.append(" <> ");
+        filter.getExpression2().accept(this, sb);
+        return sb;
+    }
+
+    @Override
+    public Object visit(final PropertyIsGreaterThan filter, final Object o) {
+        final StringBuilder sb = toStringBuilder(o);
+        filter.getExpression1().accept(this, sb);
+        sb.append(" > ");
+        filter.getExpression2().accept(this, sb);
+        return sb;
+    }
+
+    @Override
+    public Object visit(final PropertyIsGreaterThanOrEqualTo filter, final Object o) {
+        final StringBuilder sb = toStringBuilder(o);
+        filter.getExpression1().accept(this, sb);
+        sb.append(" >= ");
+        filter.getExpression2().accept(this, sb);
+        return sb;
+    }
+
+    @Override
+    public Object visit(final PropertyIsLessThan filter, final Object o) {
+        final StringBuilder sb = toStringBuilder(o);
+        filter.getExpression1().accept(this, sb);
+        sb.append(" < ");
+        filter.getExpression2().accept(this, sb);
+        return sb;
+    }
+
+    @Override
+    public Object visit(final PropertyIsLessThanOrEqualTo filter, final Object o) {
+        final StringBuilder sb = toStringBuilder(o);
+        filter.getExpression1().accept(this, sb);
+        sb.append(" <= ");
+        filter.getExpression2().accept(this, sb);
+        return sb;
+    }
+
+    @Override
+    public Object visit(final PropertyIsLike filter, final Object o) {
+        final StringBuilder sb = toStringBuilder(o);
+        final char escape = filter.getEscape().charAt(0);
+        final char wildCard = filter.getWildCard().charAt(0);
+        final char singleChar = filter.getSingleChar().charAt(0);
+        final boolean matchingCase = filter.isMatchingCase();
+        final String literal = filter.getLiteral();
+        //TODO wild card and escape encoded to sql 92
+        final String pattern = literal;
+
+        filter.getExpression().accept(this, sb);
+
+        if (matchingCase) {
+            sb.append(" LIKE ");
+        } else {
+            sb.append(" ILIKE ");
+        }
+        sb.append('\'');
+        sb.append(pattern);
+        sb.append('\'');
+        return sb;
+    }
+
+    @Override
+    public Object visit(final PropertyIsNull filter, final Object o) {
+        final StringBuilder sb = toStringBuilder(o);
+        filter.getExpression().accept(this, sb);
+        sb.append(" IS NULL");
+        return sb;
+    }
+
+    @Override
+    public Object visit(PropertyIsNil filter, Object o) {
+        final StringBuilder sb = toStringBuilder(o);
+        filter.getExpression().accept(this, sb);
+        sb.append(" IS NIL");
+        return sb;
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    // GEOMETRY FILTER /////////////////////////////////////////////////////////
+    ////////////////////////////////////////////////////////////////////////////
+    @Override
+    public Object visit(final BBOX filter, final Object o) {
+        final StringBuilder sb = toStringBuilder(o);
+
+        if (filter.getExpression1() instanceof PropertyName
+                && filter.getExpression2() instanceof Literal) {
+            //use writing : BBOX(att,v1,v2,v3,v4)
+            sb.append("BBOX(");
+            sb.append(filter.getPropertyName());
+            sb.append(',');
+            sb.append(filter.getMinX());
+            sb.append(',');
+            sb.append(filter.getMaxX());
+            sb.append(',');
+            sb.append(filter.getMinY());
+            sb.append(',');
+            sb.append(filter.getMaxY());
+            sb.append(')');
+
+        } else {
+            //use writing BBOX(exp1,exp2)
+            sb.append("BBOX(");
+            filter.getExpression1().accept(this, sb);
+            sb.append(',');
+            filter.getExpression2().accept(this, sb);
+            sb.append(')');
+        }
+
+        return sb;
+    }
+
+    @Override
+    public Object visit(final Beyond filter, final Object o) {
+        final StringBuilder sb = toStringBuilder(o);
+        sb.append("BEYOND(");
+        filter.getExpression1().accept(this, sb);
+        sb.append(',');
+        filter.getExpression2().accept(this, sb);
+        sb.append(')');
+        return sb;
+    }
+
+    @Override
+    public Object visit(final Contains filter, final Object o) {
+        final StringBuilder sb = toStringBuilder(o);
+        sb.append("CONTAINS(");
+        filter.getExpression1().accept(this, sb);
+        sb.append(',');
+        filter.getExpression2().accept(this, sb);
+        sb.append(')');
+        return sb;
+    }
+
+    @Override
+    public Object visit(final Crosses filter, final Object o) {
+        final StringBuilder sb = toStringBuilder(o);
+        sb.append("CROSSES(");
+        filter.getExpression1().accept(this, sb);
+        sb.append(',');
+        filter.getExpression2().accept(this, sb);
+        sb.append(')');
+        return sb;
+    }
+
+    @Override
+    public Object visit(final Disjoint filter, final Object o) {
+        final StringBuilder sb = toStringBuilder(o);
+        sb.append("DISJOINT(");
+        filter.getExpression1().accept(this, sb);
+        sb.append(',');
+        filter.getExpression2().accept(this, sb);
+        sb.append(')');
+        return sb;
+    }
+
+    @Override
+    public Object visit(final DWithin filter, final Object o) {
+        final StringBuilder sb = toStringBuilder(o);
+        sb.append("DWITHIN(");
+        filter.getExpression1().accept(this, sb);
+        sb.append(',');
+        filter.getExpression2().accept(this, sb);
+        sb.append(')');
+        return sb;
+    }
+
+    @Override
+    public Object visit(final Equals filter, final Object o) {
+        final StringBuilder sb = toStringBuilder(o);
+        sb.append("EQUALS(");
+        filter.getExpression1().accept(this, sb);
+        sb.append(',');
+        filter.getExpression2().accept(this, sb);
+        sb.append(')');
+        return sb;
+    }
+
+    @Override
+    public Object visit(final Intersects filter, final Object o) {
+        final StringBuilder sb = toStringBuilder(o);
+        sb.append("INTERSECTS(");
+        filter.getExpression1().accept(this, sb);
+        sb.append(',');
+        filter.getExpression2().accept(this, sb);
+        sb.append(')');
+        return sb;
+    }
+
+    @Override
+    public Object visit(final Overlaps filter, final Object o) {
+        final StringBuilder sb = toStringBuilder(o);
+        sb.append("OVERLAPS(");
+        filter.getExpression1().accept(this, sb);
+        sb.append(',');
+        filter.getExpression2().accept(this, sb);
+        sb.append(')');
+        return sb;
+    }
+
+    @Override
+    public Object visit(final Touches filter, final Object o) {
+        final StringBuilder sb = toStringBuilder(o);
+        sb.append("TOUCHES(");
+        filter.getExpression1().accept(this, sb);
+        sb.append(',');
+        filter.getExpression2().accept(this, sb);
+        sb.append(')');
+        return sb;
+    }
+
+    @Override
+    public Object visit(final Within filter, final Object o) {
+        final StringBuilder sb = toStringBuilder(o);
+        sb.append("WITHIN(");
+        filter.getExpression1().accept(this, sb);
+        sb.append(',');
+        filter.getExpression2().accept(this, sb);
+        sb.append(')');
+        return sb;
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    // TEMPORAL FILTER /////////////////////////////////////////////////////////
+    ////////////////////////////////////////////////////////////////////////////
+
+    @Override
+    public Object visit(After filter, Object o) {
+        final StringBuilder sb = toStringBuilder(o);
+        filter.getExpression1().accept(this, sb);
+        sb.append(" AFTER ");
+        filter.getExpression2().accept(this, sb);
+        return sb;
+    }
+
+    @Override
+    public Object visit(AnyInteracts filter, Object o) {
+        final StringBuilder sb = toStringBuilder(o);
+        filter.getExpression1().accept(this, sb);
+        sb.append(" ANYINTERACTS ");
+        filter.getExpression2().accept(this, sb);
+        return sb;
+    }
+
+    @Override
+    public Object visit(Before filter, Object o) {
+        final StringBuilder sb = toStringBuilder(o);
+        filter.getExpression1().accept(this, sb);
+        sb.append(" BEFORE ");
+        filter.getExpression2().accept(this, sb);
+        return sb;
+    }
+
+    @Override
+    public Object visit(Begins filter, Object o) {
+        final StringBuilder sb = toStringBuilder(o);
+        filter.getExpression1().accept(this, sb);
+        sb.append(" BEGINS ");
+        filter.getExpression2().accept(this, sb);
+        return sb;
+    }
+
+    @Override
+    public Object visit(BegunBy filter, Object o) {
+        final StringBuilder sb = toStringBuilder(o);
+        filter.getExpression1().accept(this, sb);
+        sb.append(" BEGUNBY ");
+        filter.getExpression2().accept(this, sb);
+        return sb;
+    }
+
+    @Override
+    public Object visit(During filter, Object o) {
+        final StringBuilder sb = toStringBuilder(o);
+        filter.getExpression1().accept(this, sb);
+        sb.append(" DURING ");
+        filter.getExpression2().accept(this, sb);
+        return sb;
+    }
+
+    @Override
+    public Object visit(EndedBy filter, Object o) {
+        final StringBuilder sb = toStringBuilder(o);
+        filter.getExpression1().accept(this, sb);
+        sb.append(" ENDEDBY ");
+        filter.getExpression2().accept(this, sb);
+        return sb;
+    }
+
+    @Override
+    public Object visit(Ends filter, Object o) {
+        final StringBuilder sb = toStringBuilder(o);
+        filter.getExpression1().accept(this, sb);
+        sb.append(" ENDS ");
+        filter.getExpression2().accept(this, sb);
+        return sb;
+    }
+
+    @Override
+    public Object visit(Meets filter, Object o) {
+        final StringBuilder sb = toStringBuilder(o);
+        filter.getExpression1().accept(this, sb);
+        sb.append(" MEETS ");
+        filter.getExpression2().accept(this, sb);
+        return sb;
+    }
+
+    @Override
+    public Object visit(MetBy filter, Object o) {
+        final StringBuilder sb = toStringBuilder(o);
+        filter.getExpression1().accept(this, sb);
+        sb.append(" METBY ");
+        filter.getExpression2().accept(this, sb);
+        return sb;
+    }
+
+    @Override
+    public Object visit(OverlappedBy filter, Object o) {
+        final StringBuilder sb = toStringBuilder(o);
+        filter.getExpression1().accept(this, sb);
+        sb.append(" OVERLAPPEDBY ");
+        filter.getExpression2().accept(this, sb);
+        return sb;
+    }
+
+    @Override
+    public Object visit(TContains filter, Object o) {
+        final StringBuilder sb = toStringBuilder(o);
+        filter.getExpression1().accept(this, sb);
+        sb.append(" TCONTAINS ");
+        filter.getExpression2().accept(this, sb);
+        return sb;
+    }
+
+    @Override
+    public Object visit(TEquals filter, Object o) {
+        final StringBuilder sb = toStringBuilder(o);
+        filter.getExpression1().accept(this, sb);
+        sb.append(" TEQUALS ");
+        filter.getExpression2().accept(this, sb);
+        return sb;
+    }
+
+    @Override
+    public Object visit(TOverlaps filter, Object o) {
+        final StringBuilder sb = toStringBuilder(o);
+        filter.getExpression1().accept(this, sb);
+        sb.append(" TOVERLAPS ");
+        filter.getExpression2().accept(this, sb);
+        return sb;
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    // EXPRESSIONS /////////////////////////////////////////////////////////////
+    ////////////////////////////////////////////////////////////////////////////
+    @Override
+    public Object visit(final Literal exp, final Object o) {
+        final StringBuilder sb = toStringBuilder(o);
+
+        final Object value = exp.getValue();
+        if (value instanceof Number) {
+            final Number num = (Number) value;
+            sb.append(num.toString());
+        } else if (value instanceof Date) {
+            final Date date = (Date) value;
+            sb.append(StandardDateFormat.FORMAT.format(date.toInstant()));
+        } else if (value instanceof Geometry) {
+            final Geometry geometry = (Geometry) value;
+            final WKTWriter writer = new WKTWriter();
+            final String wkt = writer.write(geometry);
+            sb.append(wkt);
+        } else {
+            sb.append('\'').append(value != null ? value.toString() : null).append('\'');
+        }
+        return sb;
+    }
+
+    @Override
+    public Object visit(final PropertyName exp, final Object o) {
+        final StringBuilder sb = toStringBuilder(o);
+        final String name = exp.getPropertyName();
+        if (patternPropertyName.matcher(name).find()) {
+            //escape for special chars
+            sb.append('"').append(name).append('"');
+        } else {
+            sb.append(name);
+        }
+        return sb;
+    }
+
+    @Override
+    public Object visit(final Add exp, final Object o) {
+        final StringBuilder sb = toStringBuilder(o);
+        exp.getExpression1().accept(this, sb);
+        sb.append(" + ");
+        exp.getExpression2().accept(this, sb);
+        return sb;
+    }
+
+    @Override
+    public Object visit(final Divide exp, final Object o) {
+        final StringBuilder sb = toStringBuilder(o);
+        exp.getExpression1().accept(this, sb);
+        sb.append(" / ");
+        exp.getExpression2().accept(this, sb);
+        return sb;
+    }
+
+    @Override
+    public Object visit(final Multiply exp, final Object o) {
+        final StringBuilder sb = toStringBuilder(o);
+        exp.getExpression1().accept(this, sb);
+        sb.append(" * ");
+        exp.getExpression2().accept(this, sb);
+        return sb;
+    }
+
+    @Override
+    public Object visit(final Subtract exp, final Object o) {
+        final StringBuilder sb = toStringBuilder(o);
+        exp.getExpression1().accept(this, sb);
+        sb.append(" - ");
+        exp.getExpression2().accept(this, sb);
+        return sb;
+    }
+
+    @Override
+    public Object visit(final Function exp, final Object o) {
+        final StringBuilder sb = toStringBuilder(o);
+        sb.append(exp.getName());
+        sb.append('(');
+        final List<Expression> exps = exp.getParameters();
+        if (exps != null) {
+            final int size = exps.size();
+            if (size == 1) {
+                exps.get(0).accept(this, sb);
+            } else if (size > 1) {
+                for (int i = 0, n = size - 1; i < n; i++) {
+                    exps.get(i).accept(this, sb);
+                    sb.append(" , ");
+                }
+                exps.get(size - 1).accept(this, sb);
+            }
+        }
+        sb.append(')');
+        return sb;
+    }
+
+    @Override
+    public Object visit(final NilExpression exp, final Object o) {
+        throw new UnsupportedOperationException("NilExpression not supported in CQL.");
+    }
+
+}
diff --git a/core/sis-cql/src/test/java/org/apache/sis/cql/CQLTestSuite.java b/core/sis-cql/src/test/java/org/apache/sis/cql/CQLTestSuite.java
new file mode 100644
index 0000000..6bf9783
--- /dev/null
+++ b/core/sis-cql/src/test/java/org/apache/sis/cql/CQLTestSuite.java
@@ -0,0 +1,39 @@
+/*
+ * 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.sis.cql;
+
+import org.apache.sis.test.TestSuite;
+import org.junit.runners.Suite;
+
+
+/**
+ * All tests from the {@code sis-cql} module.
+ *
+ * @author  Johann Sorel (Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+@Suite.SuiteClasses({
+    org.apache.sis.cql.ExpressionReadingTest.class,
+    org.apache.sis.cql.ExpressionWritingTest.class,
+    org.apache.sis.cql.FilterReadingTest.class,
+    org.apache.sis.cql.FilterWritingTest.class,
+})
+public final strictfp class CQLTestSuite extends TestSuite {
+
+}
diff --git a/core/sis-cql/src/test/java/org/apache/sis/cql/ExpressionReadingTest.java b/core/sis-cql/src/test/java/org/apache/sis/cql/ExpressionReadingTest.java
new file mode 100644
index 0000000..68b7cc2
--- /dev/null
+++ b/core/sis-cql/src/test/java/org/apache/sis/cql/ExpressionReadingTest.java
@@ -0,0 +1,618 @@
+/*
+ * 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.sis.cql;
+
+import java.text.ParseException;
+import org.apache.sis.filter.DefaultFilterFactory;
+import static org.junit.Assert.*;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.locationtech.jts.geom.Coordinate;
+import org.locationtech.jts.geom.Geometry;
+import org.locationtech.jts.geom.GeometryCollection;
+import org.locationtech.jts.geom.GeometryFactory;
+import org.locationtech.jts.geom.LineString;
+import org.locationtech.jts.geom.LinearRing;
+import org.locationtech.jts.geom.MultiLineString;
+import org.locationtech.jts.geom.MultiPoint;
+import org.locationtech.jts.geom.MultiPolygon;
+import org.locationtech.jts.geom.Point;
+import org.locationtech.jts.geom.Polygon;
+import org.opengis.filter.FilterFactory2;
+import org.opengis.filter.expression.Add;
+import org.opengis.filter.expression.Divide;
+import org.opengis.filter.expression.Function;
+import org.opengis.filter.expression.Literal;
+import org.opengis.filter.expression.Multiply;
+import org.opengis.filter.expression.PropertyName;
+import org.opengis.filter.expression.Subtract;
+
+/**
+ * Test reading CQL expressions.
+ *
+ * @author  Johann Sorel (Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+public class ExpressionReadingTest {
+
+    private final FilterFactory2 FF = new DefaultFilterFactory();
+    private final GeometryFactory GF = new GeometryFactory();
+
+    @Test
+    public void testPropertyName1() throws CQLException{
+        final String cql = "geom";
+        final Object obj = CQL.parseExpression(cql);
+        assertTrue(obj instanceof PropertyName);
+        final PropertyName expression = (PropertyName) obj;
+        assertEquals("geom", expression.getPropertyName());
+    }
+
+    @Test
+    public void testPropertyName2() throws CQLException{
+        final String cql = "\"geom\"";
+        final Object obj = CQL.parseExpression(cql);
+        assertTrue(obj instanceof PropertyName);
+        final PropertyName expression = (PropertyName) obj;
+        assertEquals("geom", expression.getPropertyName());
+    }
+
+    @Test
+    public void testPropertyName3() throws CQLException{
+        final String cql = "ùth{e_$uglY^_pr@perté";
+        final Object obj = CQL.parseExpression(cql);
+        assertTrue(obj instanceof PropertyName);
+        final PropertyName expression = (PropertyName) obj;
+        assertEquals("ùth{e_$uglY^_pr@perté", expression.getPropertyName());
+    }
+
+    @Test
+    public void testInteger() throws CQLException{
+        final String cql = "15";
+        final Object obj = CQL.parseExpression(cql);
+        assertTrue(obj instanceof Literal);
+        final Literal expression = (Literal) obj;
+        assertEquals(Integer.valueOf(15), expression.getValue());
+    }
+
+    @Test
+    public void testNegativeInteger() throws CQLException{
+        final String cql = "-15";
+        final Object obj = CQL.parseExpression(cql);
+        assertTrue(obj instanceof Literal);
+        final Literal expression = (Literal) obj;
+        assertEquals(Integer.valueOf(-15), expression.getValue());
+    }
+
+    @Test
+    public void testDecimal1() throws CQLException{
+        final String cql = "3.14";
+        final Object obj = CQL.parseExpression(cql);
+        assertTrue(obj instanceof Literal);
+        final Literal expression = (Literal) obj;
+        assertEquals(Double.valueOf(3.14), expression.getValue());
+    }
+
+    @Test
+    public void testDecimal2() throws CQLException{
+        final String cql = "9e-1";
+        final Object obj = CQL.parseExpression(cql);
+        assertTrue(obj instanceof Literal);
+        final Literal expression = (Literal) obj;
+        assertEquals(Double.valueOf(9e-1), expression.getValue());
+    }
+
+    @Test
+    public void testNegativeDecimal() throws CQLException{
+        final String cql = "-3.14";
+        final Object obj = CQL.parseExpression(cql);
+        assertTrue(obj instanceof Literal);
+        final Literal expression = (Literal) obj;
+        assertEquals(Double.valueOf(-3.14), expression.getValue());
+    }
+
+    @Test
+    public void testText() throws CQLException{
+        final String cql = "'hello world'";
+        final Object obj = CQL.parseExpression(cql);
+        assertTrue(obj instanceof Literal);
+        final Literal expression = (Literal) obj;
+        assertEquals("hello world", expression.getValue());
+    }
+
+    @Test
+    public void testText2() throws CQLException{
+        final String cql = "'Valle d\\'Aosta'";
+        final Object obj = CQL.parseExpression(cql);
+        assertTrue(obj instanceof Literal);
+        final Literal expression = (Literal) obj;
+        assertEquals("Valle d'Aosta", expression.getValue());
+    }
+
+    @Test
+    public void testText3() throws CQLException{
+        final String cql = "'Valle d\\'Aosta/Vallée d\\'Aoste'";
+        final Object obj = CQL.parseExpression(cql);
+        assertTrue(obj instanceof Literal);
+        final Literal expression = (Literal) obj;
+        assertEquals("Valle d'Aosta/Vallée d'Aoste", expression.getValue());
+    }
+
+    @Ignore
+    @Test
+    public void testDate() throws CQLException, ParseException{
+        //dates are expected to be formated in ISO 8601 : yyyy-MM-dd'T'HH:mm:ss'Z'
+        final String cql = "2012-03-21T05:42:36Z";
+        final Object obj = CQL.parseExpression(cql);
+        assertTrue(obj instanceof Literal);
+        final Literal expression = (Literal) obj;
+        assertEquals(TemporalUtilities.parseDate("2012-03-21T05:42:36Z"), expression.getValue());
+    }
+
+    @Ignore
+    @Test
+    public void testDuration() throws CQLException, ParseException{
+        final String cql = "P7Y6M5D4H3M2S";
+        final Object obj = CQL.parseExpression(cql);
+        assertTrue(obj instanceof Literal);
+        final Literal expression = (Literal) obj;
+        final long duration = (Long) expression.getValue();
+
+        assertEquals(236966582000l, duration);
+    }
+
+    @Ignore
+    @Test
+    public void testDuration2() throws CQLException, ParseException{
+        final String cql = "T4H3M2S";
+        final Object obj = CQL.parseExpression(cql);
+        assertTrue(obj instanceof Literal);
+        final Literal expression = (Literal) obj;
+        final long duration = (Long) expression.getValue();
+
+        assertEquals(14582000,duration);
+    }
+
+    @Test
+    public void testAddition() throws CQLException{
+        final String cql = "3 + 2";
+        final Object obj = CQL.parseExpression(cql);
+        assertTrue(obj instanceof Add);
+        final Add expression = (Add) obj;
+        assertEquals(FF.add(FF.literal(3), FF.literal(2)), expression);
+    }
+
+    @Ignore
+    @Test
+    public void testAddition2() throws CQLException{
+        final String cql = "'test' + '23'";
+        final Object obj = CQL.parseExpression(cql);
+        assertTrue(obj instanceof Add);
+        final Add expression = (Add) obj;
+
+        Object res = expression.evaluate(null);
+        assertEquals("test23", res);
+    }
+
+    @Test
+    public void testSubstract() throws CQLException{
+        final String cql = "3 - 2";
+        final Object obj = CQL.parseExpression(cql);
+        assertTrue(obj instanceof Subtract);
+        final Subtract expression = (Subtract) obj;
+        assertEquals(FF.subtract(FF.literal(3), FF.literal(2)), expression);
+    }
+
+    @Test
+    public void testMultiply() throws CQLException{
+        final String cql = "3 * 2";
+        final Object obj = CQL.parseExpression(cql);
+        assertTrue(obj instanceof Multiply);
+        final Multiply expression = (Multiply) obj;
+        assertEquals(FF.multiply(FF.literal(3), FF.literal(2)), expression);
+    }
+
+    @Test
+    public void testDivide() throws CQLException{
+        final String cql = "3 / 2";
+        final Object obj = CQL.parseExpression(cql);
+        assertTrue(obj instanceof Divide);
+        final Divide expression = (Divide) obj;
+        assertEquals(FF.divide(FF.literal(3), FF.literal(2)), expression);
+    }
+
+    @Ignore
+    @Test
+    public void testFunction1() throws CQLException{
+        final String cql = "max(\"att\",15)";
+        final Object obj = CQL.parseExpression(cql);
+        assertTrue(obj instanceof Function);
+        final Function expression = (Function) obj;
+        assertEquals(FF.function("max",FF.property("att"), FF.literal(15)), expression);
+    }
+
+    @Ignore
+    @Test
+    public void testFunction2() throws CQLException{
+        final String cql = "min(\"att\",cos(3.14))";
+        final Object obj = CQL.parseExpression(cql);
+        assertTrue(obj instanceof Function);
+        final Function expression = (Function) obj;
+        assertEquals(FF.function("min",FF.property("att"), FF.function("cos",FF.literal(3.14d))), expression);
+    }
+
+    @Test
+    public void testGeometryPoint() throws CQLException{
+        final String cql = "POINT(15 30)";
+        final Object obj = CQL.parseExpression(cql);
+        assertTrue(obj instanceof Literal);
+        final Literal expression = (Literal) obj;
+        final Geometry geom =  GF.createPoint(new Coordinate(15, 30));
+        assertTrue(geom.equals((Geometry)expression.getValue()));
+    }
+
+    @Test
+    public void testGeometryPointEmpty() throws CQLException{
+        final String cql = "POINT EMPTY";
+        final Object obj = CQL.parseExpression(cql);
+        assertTrue(obj instanceof Literal);
+        final Literal expression = (Literal) obj;
+        final Geometry geom = (Geometry)expression.getValue();
+        assertTrue(geom instanceof Point);
+        assertTrue(geom.isEmpty());
+    }
+
+    @Test
+    public void testGeometryMPoint() throws CQLException{
+        final String cql = "MULTIPOINT(15 30, 45 60)";
+        final Object obj = CQL.parseExpression(cql);
+        assertTrue(obj instanceof Literal);
+        final Literal expression = (Literal) obj;
+        final Geometry geom =  GF.createMultiPoint(
+                new Coordinate[]{
+                    new Coordinate(15, 30),
+                    new Coordinate(45, 60)
+                });
+        assertTrue(geom.equals((Geometry)expression.getValue()));
+    }
+
+    @Test
+    public void testGeometryMPointEmpty() throws CQLException{
+        final String cql = "MULTIPOINT EMPTY";
+        final Object obj = CQL.parseExpression(cql);
+        assertTrue(obj instanceof Literal);
+        final Literal expression = (Literal) obj;
+        final Geometry geom = (Geometry)expression.getValue();
+        assertTrue(geom instanceof MultiPoint);
+        assertTrue(geom.isEmpty());
+    }
+
+    @Test
+    public void testGeometryLineString() throws CQLException{
+        final String cql = "LINESTRING(10 20, 30 40, 50 60)";
+        final Object obj = CQL.parseExpression(cql);
+        assertTrue(obj instanceof Literal);
+        final Literal expression = (Literal) obj;
+        final Geometry geom =  GF.createLineString(
+                new Coordinate[]{
+                    new Coordinate(10, 20),
+                    new Coordinate(30, 40),
+                    new Coordinate(50, 60)
+                });
+        assertTrue(geom.equals((Geometry)expression.getValue()));
+    }
+
+    @Test
+    public void testGeometryLineStringEmpty() throws CQLException{
+        final String cql = "LINESTRING EMPTY";
+        final Object obj = CQL.parseExpression(cql);
+        assertTrue(obj instanceof Literal);
+        final Literal expression = (Literal) obj;
+        final Geometry geom = (Geometry)expression.getValue();
+        assertTrue(geom instanceof LineString);
+        assertTrue(geom.isEmpty());
+    }
+
+    @Test
+    public void testGeometryMLineString() throws CQLException{
+        final String cql = "MULTILINESTRING((10 20, 30 40, 50 60),(70 80, 90 100, 110 120))";
+        final Object obj = CQL.parseExpression(cql);
+        assertTrue(obj instanceof Literal);
+        final Literal expression = (Literal) obj;
+        final Geometry geom =  GF.createMultiLineString(
+                new LineString[]{
+                    GF.createLineString(
+                        new Coordinate[]{
+                            new Coordinate(10, 20),
+                            new Coordinate(30, 40),
+                            new Coordinate(50, 60)
+                        }),
+                    GF.createLineString(
+                        new Coordinate[]{
+                            new Coordinate(70, 80),
+                            new Coordinate(90, 100),
+                            new Coordinate(110, 120)
+                        })
+                    }
+                );
+        assertTrue(geom.equals((Geometry)expression.getValue()));
+    }
+
+    @Test
+    public void testGeometryMLineStringEmpty() throws CQLException{
+        final String cql = "MULTILINESTRING EMPTY";
+        final Object obj = CQL.parseExpression(cql);
+        assertTrue(obj instanceof Literal);
+        final Literal expression = (Literal) obj;
+        final Geometry geom = (Geometry)expression.getValue();
+        assertTrue(geom instanceof MultiLineString);
+        assertTrue(geom.isEmpty());
+    }
+
+    @Test
+    public void testGeometryPolygon() throws CQLException{
+        final String cql = "POLYGON((10 20, 30 40, 50 60, 10 20), (70 80, 90 100, 110 120, 70 80))";
+        final Object obj = CQL.parseExpression(cql);
+        assertTrue(obj instanceof Literal);
+        final Literal expression = (Literal) obj;
+        final Geometry geom =  GF.createPolygon(
+                GF.createLinearRing(
+                    new Coordinate[]{
+                        new Coordinate(10, 20),
+                        new Coordinate(30, 40),
+                        new Coordinate(50, 60),
+                        new Coordinate(10, 20)
+                    }),
+                new LinearRing[]{
+                    GF.createLinearRing(
+                        new Coordinate[]{
+                            new Coordinate(70, 80),
+                            new Coordinate(90, 100),
+                            new Coordinate(110, 120),
+                            new Coordinate(70, 80)
+                        })
+                    }
+                );
+        assertTrue(geom.equals((Geometry)expression.getValue()));
+    }
+
+    @Test
+    public void testGeometryPolygonEmpty() throws CQLException{
+        final String cql = "POLYGON EMPTY";
+        final Object obj = CQL.parseExpression(cql);
+        assertTrue(obj instanceof Literal);
+        final Literal expression = (Literal) obj;
+        final Geometry geom = (Geometry)expression.getValue();
+        assertTrue(geom instanceof Polygon);
+        assertTrue(geom.isEmpty());
+    }
+
+    @Test
+    public void testGeometryMPolygon() throws CQLException{
+        final String cql = "MULTIPOLYGON("
+                + "((10 20, 30 40, 50 60, 10 20), (70 80, 90 100, 110 120, 70 80)),"
+                + "((11 21, 31 41, 51 61, 11 21), (71 81, 91 101, 111 121, 71 81))"
+                + ")";
+        final Object obj = CQL.parseExpression(cql);
+        assertTrue(obj instanceof Literal);
+        final Literal expression = (Literal) obj;
+        final Polygon geom1 = GF.createPolygon(
+                GF.createLinearRing(
+                    new Coordinate[]{
+                        new Coordinate(10, 20),
+                        new Coordinate(30, 40),
+                        new Coordinate(50, 60),
+                        new Coordinate(10, 20)
+                    }),
+                new LinearRing[]{
+                    GF.createLinearRing(
+                        new Coordinate[]{
+                            new Coordinate(70, 80),
+                            new Coordinate(90, 100),
+                            new Coordinate(110, 120),
+                            new Coordinate(70, 80)
+                        })
+                    }
+                );
+        final Polygon geom2 = GF.createPolygon(
+                GF.createLinearRing(
+                    new Coordinate[]{
+                        new Coordinate(11, 21),
+                        new Coordinate(31, 41),
+                        new Coordinate(51, 61),
+                        new Coordinate(11, 21)
+                    }),
+                new LinearRing[]{
+                    GF.createLinearRing(
+                        new Coordinate[]{
+                            new Coordinate(71, 81),
+                            new Coordinate(91, 101),
+                            new Coordinate(111, 121),
+                            new Coordinate(71, 81)
+                        })
+                    }
+                );
+        final Geometry geom = GF.createMultiPolygon(new Polygon[]{geom1,geom2});
+        assertTrue(geom.equals((Geometry)expression.getValue()));
+    }
+
+    @Test
+    public void testGeometryMPolygonEmpty() throws CQLException{
+        final String cql = "MULTIPOLYGON EMPTY";
+        final Object obj = CQL.parseExpression(cql);
+        assertTrue(obj instanceof Literal);
+        final Literal expression = (Literal) obj;
+        final Geometry geom = (Geometry)expression.getValue();
+        assertTrue(geom instanceof MultiPolygon);
+        assertTrue(geom.isEmpty());
+    }
+
+    @Test
+    public void testGeometryCollection() throws CQLException{
+        final String cql = "GEOMETRYCOLLECTION( POINT(15 30), LINESTRING(10 20, 30 40, 50 60) )";
+        final Object obj = CQL.parseExpression(cql);
+        assertTrue(obj instanceof Literal);
+        final Literal expression = (Literal) obj;
+        final Geometry geom1 =  GF.createPoint(new Coordinate(15, 30));
+        final Geometry geom2 =  GF.createLineString(
+                new Coordinate[]{
+                    new Coordinate(10, 20),
+                    new Coordinate(30, 40),
+                    new Coordinate(50, 60)
+                });
+        final GeometryCollection geom =  GF.createGeometryCollection(new Geometry[]{geom1,geom2});
+        final GeometryCollection returned = (GeometryCollection)expression.getValue();
+        assertEquals(geom.getNumGeometries(), returned.getNumGeometries());
+        assertEquals(geom.getGeometryN(0), returned.getGeometryN(0));
+        assertEquals(geom.getGeometryN(1), returned.getGeometryN(1));
+    }
+
+    @Test
+    public void testGeometryCollectionEmpty() throws CQLException{
+        final String cql = "GEOMETRYCOLLECTION EMPTY";
+        final Object obj = CQL.parseExpression(cql);
+        assertTrue(obj instanceof Literal);
+        final Literal expression = (Literal) obj;
+        final Geometry geom = (Geometry)expression.getValue();
+        assertTrue(geom instanceof GeometryCollection);
+        assertTrue(geom.isEmpty());
+    }
+
+    @Test
+    public void testGeometryEnvelope() throws CQLException{
+        final String cql = "ENVELOPE(10, 20, 40, 30)";
+        final Object obj = CQL.parseExpression(cql);
+        assertTrue(obj instanceof Literal);
+        final Literal expression = (Literal) obj;
+        final Geometry geom =  GF.createPolygon(
+                GF.createLinearRing(
+                    new Coordinate[]{
+                        new Coordinate(10, 40),
+                        new Coordinate(20, 40),
+                        new Coordinate(20, 30),
+                        new Coordinate(10, 30),
+                        new Coordinate(10, 40)
+                    }),
+                new LinearRing[0]
+                );
+        assertTrue(geom.equals((Geometry)expression.getValue()));
+    }
+
+    @Test
+    public void testGeometryEnvelopeEmpty() throws CQLException{
+        final String cql = "ENVELOPE EMPTY";
+        final Object obj = CQL.parseExpression(cql);
+        assertTrue(obj instanceof Literal);
+        final Literal expression = (Literal) obj;
+        final Geometry geom = (Geometry)expression.getValue();
+        assertTrue(geom instanceof Polygon);
+        assertTrue(geom.isEmpty());
+    }
+
+    @Test
+    public void testCombine1() throws CQLException{
+        final String cql = "((3*1)+(2-6))/4";
+        final Object obj = CQL.parseExpression(cql);
+        assertTrue(obj instanceof Divide);
+        final Divide expression = (Divide) obj;
+        assertEquals(
+                FF.divide(
+                    FF.add(
+                        FF.multiply(FF.literal(3), FF.literal(1)),
+                        FF.subtract(FF.literal(2), FF.literal(6))
+                        ),
+                    FF.literal(4))
+                , expression);
+    }
+
+    @Test
+    public void testCombine2() throws CQLException{
+        final String cql = "3*1+2/4";
+        final Object obj = CQL.parseExpression(cql);
+        assertTrue(obj instanceof Add);
+        final Add rootAdd = (Add) obj;
+
+        assertEquals(
+                    FF.add(
+                        FF.multiply(FF.literal(3), FF.literal(1)),
+                        FF.divide(FF.literal(2), FF.literal(4))
+                        )
+                , rootAdd);
+
+    }
+
+    @Ignore
+    @Test
+    public void testCombine3() throws CQLException{
+        final String cql = "3*max(val,15)+2/4";
+        final Object obj = CQL.parseExpression(cql);
+        assertTrue(obj instanceof Add);
+        final Add rootAdd = (Add) obj;
+
+        assertEquals(
+                    FF.add(
+                        FF.multiply(
+                            FF.literal(3),
+                            FF.function("max", FF.property("val"),FF.literal(15))
+                        ),
+                        FF.divide(FF.literal(2), FF.literal(4))
+                        )
+                , rootAdd);
+
+    }
+
+    @Ignore
+    @Test
+    public void testCombine4() throws CQLException{
+        final String cql = "3 * max ( val , 15 ) + 2 / 4";
+        final Object obj = CQL.parseExpression(cql);
+        assertTrue(obj instanceof Add);
+        final Add rootAdd = (Add) obj;
+
+        assertEquals(
+                    FF.add(
+                        FF.multiply(
+                            FF.literal(3),
+                            FF.function("max", FF.property("val"),FF.literal(15))
+                        ),
+                        FF.divide(FF.literal(2), FF.literal(4))
+                        )
+                , rootAdd);
+
+    }
+
+    @Test
+    public void testCombine5() throws CQLException{
+        final String cql = "(\"NB-Curistes\"*50)/12000";
+        final Object obj = CQL.parseExpression(cql);
+        assertTrue(obj instanceof Divide);
+        final Divide result = (Divide) obj;
+
+        assertEquals(
+                FF.divide(
+                        FF.multiply(
+                                FF.property("NB-Curistes"),
+                                FF.literal(50)
+                        ),
+                        FF.literal(12000)
+                )
+                , result);
+
+    }
+
+}
diff --git a/core/sis-cql/src/test/java/org/apache/sis/cql/ExpressionWritingTest.java b/core/sis-cql/src/test/java/org/apache/sis/cql/ExpressionWritingTest.java
new file mode 100644
index 0000000..708064f
--- /dev/null
+++ b/core/sis-cql/src/test/java/org/apache/sis/cql/ExpressionWritingTest.java
@@ -0,0 +1,344 @@
+/*
+ * 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.sis.cql;
+
+import java.text.ParseException;
+import org.apache.sis.filter.DefaultFilterFactory;
+import static org.junit.Assert.*;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.locationtech.jts.geom.Coordinate;
+import org.locationtech.jts.geom.Geometry;
+import org.locationtech.jts.geom.GeometryFactory;
+import org.locationtech.jts.geom.LineString;
+import org.locationtech.jts.geom.LinearRing;
+import org.locationtech.jts.geom.Polygon;
+import org.opengis.filter.FilterFactory2;
+import org.opengis.filter.expression.Expression;
+
+/**
+ * Test writing in CQL expressions.
+ *
+ * @author  Johann Sorel (Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+public class ExpressionWritingTest {
+
+    private final FilterFactory2 FF = new DefaultFilterFactory();
+    private final GeometryFactory GF = new GeometryFactory();
+
+    @Test
+    public void testPropertyName1() throws CQLException{
+        final Expression exp = FF.property("geom");
+        final String cql = CQL.write(exp);
+        assertNotNull(cql);
+        assertEquals("geom", cql);
+    }
+
+    @Test
+    public void testPropertyName2() throws CQLException{
+        final Expression exp = FF.property("the geom");
+        final String cql = CQL.write(exp);
+        assertNotNull(cql);
+        assertEquals("\"the geom\"", cql);
+    }
+
+    @Test
+    public void testInteger() throws CQLException{
+        final Expression exp = FF.literal(15);
+        final String cql = CQL.write(exp);
+        assertNotNull(cql);
+        assertEquals("15", cql);
+    }
+
+    @Test
+    public void testNegativeInteger() throws CQLException{
+        final Expression exp = FF.literal(-15);
+        final String cql = CQL.write(exp);
+        assertNotNull(cql);
+        assertEquals("-15", cql);
+    }
+
+    @Test
+    public void testDecimal1() throws CQLException{
+        final Expression exp = FF.literal(3.14);
+        final String cql = CQL.write(exp);
+        assertNotNull(cql);
+        assertEquals("3.14", cql);
+    }
+
+    @Test
+    public void testDecimal2() throws CQLException{
+        final Expression exp = FF.literal(9.0E-21);
+        final String cql = CQL.write(exp);
+        assertNotNull(cql);
+        assertEquals("9.0E-21", cql);
+    }
+
+    @Ignore
+    @Test
+    public void testDate() throws CQLException, ParseException{
+        final Expression exp = FF.literal(TemporalUtilities.parseDate("2012-03-21T05:42:36Z"));
+        final String cql = CQL.write(exp);
+        assertNotNull(cql);
+        assertEquals("2012-03-21T05:42:36Z", cql);
+    }
+
+
+    @Test
+    public void testNegativeDecimal() throws CQLException{
+        final Expression exp = FF.literal(-3.14);
+        final String cql = CQL.write(exp);
+        assertNotNull(cql);
+        assertEquals("-3.14", cql);
+    }
+
+    @Test
+    public void testText() throws CQLException{
+        final Expression exp = FF.literal("hello world");
+        final String cql = CQL.write(exp);
+        assertNotNull(cql);
+        assertEquals("'hello world'", cql);
+    }
+
+    @Test
+    public void testAdd() throws CQLException{
+        final Expression exp = FF.add(FF.literal(3),FF.literal(2));
+        final String cql = CQL.write(exp);
+        assertNotNull(cql);
+        assertEquals("3 + 2", cql);
+    }
+
+    @Test
+    public void testSubtract() throws CQLException{
+        final Expression exp = FF.subtract(FF.literal(3),FF.literal(2));
+        final String cql = CQL.write(exp);
+        assertNotNull(cql);
+        assertEquals("3 - 2", cql);
+    }
+
+    @Test
+    public void testMultiply() throws CQLException{
+        final Expression exp = FF.multiply(FF.literal(3),FF.literal(2));
+        final String cql = CQL.write(exp);
+        assertNotNull(cql);
+        assertEquals("3 * 2", cql);
+    }
+
+    @Test
+    public void testDivide() throws CQLException{
+        final Expression exp = FF.divide(FF.literal(3),FF.literal(2));
+        final String cql = CQL.write(exp);
+        assertNotNull(cql);
+        assertEquals("3 / 2", cql);
+    }
+
+    @Ignore
+    @Test
+    public void testFunction1() throws CQLException{
+        final Expression exp = FF.function("max",FF.property("att"), FF.literal(15));
+        final String cql = CQL.write(exp);
+        assertNotNull(cql);
+        assertEquals("max(att , 15)", cql);
+    }
+
+    @Ignore
+    @Test
+    public void testFunction2() throws CQLException{
+        final Expression exp = FF.function("min",FF.property("att"), FF.function("cos",FF.literal(3.14d)));
+        final String cql = CQL.write(exp);
+        assertNotNull(cql);
+        assertEquals("min(att , cos(3.14))", cql);
+    }
+
+    @Test
+    public void testCombine1() throws CQLException{
+        final Expression exp =
+                FF.divide(
+                    FF.add(
+                        FF.multiply(FF.literal(3), FF.literal(1)),
+                        FF.subtract(FF.literal(2), FF.literal(6))
+                        ),
+                    FF.literal(4));
+        final String cql = CQL.write(exp);
+        assertNotNull(cql);
+        assertEquals("3 * 1 + 2 - 6 / 4", cql);
+    }
+
+    @Test
+    public void testCombine2() throws CQLException{
+        final Expression exp =
+                FF.add(
+                        FF.multiply(FF.literal(3), FF.literal(1)),
+                        FF.divide(FF.literal(2), FF.literal(4))
+                        );
+        final String cql = CQL.write(exp);
+        assertNotNull(cql);
+        assertEquals("3 * 1 + 2 / 4", cql);
+
+    }
+
+    @Ignore
+    @Test
+    public void testCombine3() throws CQLException{
+        final Expression exp =
+                FF.add(
+                        FF.multiply(
+                            FF.literal(3),
+                            FF.function("max", FF.property("val"),FF.literal(15))
+                        ),
+                        FF.divide(FF.literal(2), FF.literal(4))
+                        );
+        final String cql = CQL.write(exp);
+        assertNotNull(cql);
+        assertEquals("3 * max(val , 15) + 2 / 4", cql);
+    }
+
+    @Test
+    public void testPoint() throws CQLException{
+        final Geometry geom = GF.createPoint(new Coordinate(15, 30));
+        final Expression exp = FF.literal(geom);
+        final String cql = CQL.write(exp);
+        assertNotNull(cql);
+        assertEquals("POINT (15 30)", cql);
+    }
+
+    @Test
+    public void testMPoint() throws CQLException{
+        final Geometry geom = GF.createMultiPoint(
+                new Coordinate[]{
+                    new Coordinate(15, 30),
+                    new Coordinate(45, 60)
+                });
+        final Expression exp = FF.literal(geom);
+        final String cql = CQL.write(exp);
+        assertNotNull(cql);
+        assertEquals("MULTIPOINT ((15 30), (45 60))", cql);
+    }
+
+    @Test
+    public void testLineString() throws CQLException{
+        final Geometry geom = GF.createLineString(
+                new Coordinate[]{
+                    new Coordinate(10, 20),
+                    new Coordinate(30, 40),
+                    new Coordinate(50, 60)
+                });
+        final Expression exp = FF.literal(geom);
+        final String cql = CQL.write(exp);
+        assertNotNull(cql);
+        assertEquals("LINESTRING (10 20, 30 40, 50 60)", cql);
+    }
+
+    @Test
+    public void testMLineString() throws CQLException{
+        final Geometry geom = GF.createMultiLineString(
+                new LineString[]{
+                    GF.createLineString(
+                        new Coordinate[]{
+                            new Coordinate(10, 20),
+                            new Coordinate(30, 40),
+                            new Coordinate(50, 60)
+                        }),
+                    GF.createLineString(
+                        new Coordinate[]{
+                            new Coordinate(70, 80),
+                            new Coordinate(90, 100),
+                            new Coordinate(110, 120)
+                        })
+                    }
+                );
+        final Expression exp = FF.literal(geom);
+        final String cql = CQL.write(exp);
+        assertNotNull(cql);
+        assertEquals("MULTILINESTRING ((10 20, 30 40, 50 60), (70 80, 90 100, 110 120))", cql);
+    }
+
+    @Test
+    public void testPolygon() throws CQLException{
+        final Geometry geom = GF.createPolygon(
+                GF.createLinearRing(
+                    new Coordinate[]{
+                        new Coordinate(10, 20),
+                        new Coordinate(30, 40),
+                        new Coordinate(50, 60),
+                        new Coordinate(10, 20)
+                    }),
+                new LinearRing[]{
+                    GF.createLinearRing(
+                        new Coordinate[]{
+                            new Coordinate(70, 80),
+                            new Coordinate(90, 100),
+                            new Coordinate(110, 120),
+                            new Coordinate(70, 80)
+                        })
+                    }
+                );
+        final Expression exp = FF.literal(geom);
+        final String cql = CQL.write(exp);
+        assertNotNull(cql);
+        assertEquals("POLYGON ((10 20, 30 40, 50 60, 10 20), (70 80, 90 100, 110 120, 70 80))", cql);
+    }
+
+    @Test
+    public void testMPolygon() throws CQLException{
+        final Polygon geom1 = GF.createPolygon(
+                GF.createLinearRing(
+                    new Coordinate[]{
+                        new Coordinate(10, 20),
+                        new Coordinate(30, 40),
+                        new Coordinate(50, 60),
+                        new Coordinate(10, 20)
+                    }),
+                new LinearRing[]{
+                    GF.createLinearRing(
+                        new Coordinate[]{
+                            new Coordinate(70, 80),
+                            new Coordinate(90, 100),
+                            new Coordinate(110, 120),
+                            new Coordinate(70, 80)
+                        })
+                    }
+                );
+        final Polygon geom2 = GF.createPolygon(
+                GF.createLinearRing(
+                    new Coordinate[]{
+                        new Coordinate(11, 21),
+                        new Coordinate(31, 41),
+                        new Coordinate(51, 61),
+                        new Coordinate(11, 21)
+                    }),
+                new LinearRing[]{
+                    GF.createLinearRing(
+                        new Coordinate[]{
+                            new Coordinate(71, 81),
+                            new Coordinate(91, 101),
+                            new Coordinate(111, 121),
+                            new Coordinate(71, 81)
+                        })
+                    }
+                );
+        final Geometry geom = GF.createMultiPolygon(new Polygon[]{geom1,geom2});
+        final Expression exp = FF.literal(geom);
+        final String cql = CQL.write(exp);
+        assertNotNull(cql);
+        assertEquals("MULTIPOLYGON (((10 20, 30 40, 50 60, 10 20), (70 80, 90 100, 110 120, 70 80)), ((11 21, 31 41, 51 61, 11 21), (71 81, 91 101, 111 121, 71 81)))", cql);
+    }
+
+}
diff --git a/core/sis-cql/src/test/java/org/apache/sis/cql/FilterReadingTest.java b/core/sis-cql/src/test/java/org/apache/sis/cql/FilterReadingTest.java
new file mode 100644
index 0000000..e2946a8
--- /dev/null
+++ b/core/sis-cql/src/test/java/org/apache/sis/cql/FilterReadingTest.java
@@ -0,0 +1,849 @@
+/*
+ * 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.sis.cql;
+
+import java.text.ParseException;
+import java.util.Date;
+import org.apache.sis.filter.DefaultFilterFactory;
+import org.apache.sis.internal.util.UnmodifiableArrayList;
+import static org.junit.Assert.*;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.locationtech.jts.geom.Coordinate;
+import org.locationtech.jts.geom.Geometry;
+import org.locationtech.jts.geom.GeometryFactory;
+import org.locationtech.jts.geom.LinearRing;
+import org.opengis.filter.And;
+import org.opengis.filter.Filter;
+import org.opengis.filter.FilterFactory2;
+import org.opengis.filter.Not;
+import org.opengis.filter.Or;
+import org.opengis.filter.PropertyIsBetween;
+import org.opengis.filter.PropertyIsEqualTo;
+import org.opengis.filter.PropertyIsGreaterThan;
+import org.opengis.filter.PropertyIsGreaterThanOrEqualTo;
+import org.opengis.filter.PropertyIsLessThan;
+import org.opengis.filter.PropertyIsLessThanOrEqualTo;
+import org.opengis.filter.PropertyIsLike;
+import org.opengis.filter.PropertyIsNotEqualTo;
+import org.opengis.filter.PropertyIsNull;
+import org.opengis.filter.expression.Literal;
+import org.opengis.filter.spatial.BBOX;
+import org.opengis.filter.spatial.Beyond;
+import org.opengis.filter.spatial.BinarySpatialOperator;
+import org.opengis.filter.spatial.Contains;
+import org.opengis.filter.spatial.Crosses;
+import org.opengis.filter.spatial.DWithin;
+import org.opengis.filter.spatial.Disjoint;
+import org.opengis.filter.spatial.Equals;
+import org.opengis.filter.spatial.Intersects;
+import org.opengis.filter.spatial.Overlaps;
+import org.opengis.filter.spatial.Touches;
+import org.opengis.filter.spatial.Within;
+import org.opengis.filter.temporal.After;
+import org.opengis.filter.temporal.AnyInteracts;
+import org.opengis.filter.temporal.Before;
+import org.opengis.filter.temporal.Begins;
+import org.opengis.filter.temporal.BegunBy;
+import org.opengis.filter.temporal.During;
+import org.opengis.filter.temporal.EndedBy;
+import org.opengis.filter.temporal.Ends;
+import org.opengis.filter.temporal.Meets;
+import org.opengis.filter.temporal.MetBy;
+import org.opengis.filter.temporal.OverlappedBy;
+import org.opengis.filter.temporal.TContains;
+import org.opengis.filter.temporal.TEquals;
+import org.opengis.filter.temporal.TOverlaps;
+
+/**
+ * Test reading CQL filters.
+ *
+ * @author  Johann Sorel (Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+public class FilterReadingTest {
+
+    private static final double DELTA = 0.00000001;
+    private final FilterFactory2 FF = new DefaultFilterFactory();
+    private final GeometryFactory GF = new GeometryFactory();
+    private final Geometry baseGeometry = GF.createPolygon(
+                GF.createLinearRing(
+                    new Coordinate[]{
+                        new Coordinate(10, 20),
+                        new Coordinate(30, 40),
+                        new Coordinate(50, 60),
+                        new Coordinate(10, 20)
+                    }),
+                new LinearRing[0]
+                );
+    private final Geometry baseGeometryPoint = GF.createPoint(
+                new Coordinate(12.1, 28.9));
+
+
+    @Test
+    public void testNullFilter() throws CQLException {
+        //this is not true cql but is since in commun use cases.
+        String cql = "";
+        Object obj = CQL.parseFilter(cql);
+        assertEquals(Filter.INCLUDE,obj);
+
+        cql = "*";
+        obj = CQL.parseFilter(cql);
+        assertEquals(Filter.INCLUDE,obj);
+    }
+
+    @Test
+    public void testAnd() throws CQLException {
+        final String cql = "att1 = 15 AND att2 = 30 AND att3 = 50";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof Filter);
+        final Filter filter = (Filter) obj;
+        assertEquals(
+                FF.and(
+                UnmodifiableArrayList.wrap(new Filter[] {(Filter)
+                    FF.equals(FF.property("att1"), FF.literal(15)),
+                    FF.equals(FF.property("att2"), FF.literal(30)),
+                    FF.equals(FF.property("att3"), FF.literal(50))
+                })),
+                filter);
+    }
+
+    @Test
+    public void testOr() throws CQLException {
+        final String cql = "att1 = 15 OR att2 = 30 OR att3 = 50";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof Filter);
+        final Filter filter = (Filter) obj;
+        assertEquals(
+                FF.or(
+                UnmodifiableArrayList.wrap(new Filter[] {(Filter)
+                    FF.equals(FF.property("att1"), FF.literal(15)),
+                    FF.equals(FF.property("att2"), FF.literal(30)),
+                    FF.equals(FF.property("att3"), FF.literal(50))
+                })),
+                filter);
+    }
+
+    @Ignore
+    @Test
+    public void testOrAnd1() throws CQLException {
+        final String cql = "Title = 'VMAI' OR (Title ILIKE 'LO?Li' AND DWITHIN(BoundingBox, POINT(12.1 28.9), 10, meters))";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof Filter);
+        final Filter filter = (Filter) obj;
+        assertEquals(
+                FF.or(
+                    FF.equals(FF.property("Title"), FF.literal("VMAI")),
+                    FF.and(
+                        FF.like(FF.property("Title"), "LO?Li","%","_","\\",false),
+                        FF.dwithin(FF.property("BoundingBox"), FF.literal(baseGeometryPoint), 10, "meters")
+                        )
+                ),
+                filter);
+    }
+
+    @Ignore
+    @Test
+    public void testOrAnd2() throws CQLException {
+        final Geometry geom =  GF.createPolygon(
+                GF.createLinearRing(
+                    new Coordinate[]{
+                        new Coordinate(10, 40),
+                        new Coordinate(20, 40),
+                        new Coordinate(20, 30),
+                        new Coordinate(10, 30),
+                        new Coordinate(10, 40)
+                    }),
+                new LinearRing[0]
+                );
+
+
+        final String cql = "NOT (INTERSECTS(BoundingBox, ENVELOPE(10, 20, 40, 30)) OR CONTAINS(BoundingBox, POINT(12.1 28.9))) AND BBOX(BoundingBox, 10,20,30,40)";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof Filter);
+        final Filter filter = (Filter) obj;
+        assertEquals(
+                FF.and(
+                    FF.not(
+                        FF.or(
+                            FF.intersects(FF.property("BoundingBox"), FF.literal(geom)),
+                            FF.contains(FF.property("BoundingBox"), FF.literal(baseGeometryPoint))
+                            )
+                    ),
+                    FF.bbox("BoundingBox",10,20,30,40,"")
+                ),
+                filter);
+    }
+
+    @Test
+    public void testNot() throws CQLException {
+        final String cql = "NOT att = 15";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof Not);
+        final Not filter = (Not) obj;
+        assertEquals(FF.not(FF.equals(FF.property("att"), FF.literal(15))), filter);
+    }
+
+    @Ignore
+    @Test
+    public void testPropertyIsBetween() throws CQLException {
+        final String cql = "att BETWEEN 15 AND 30";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof PropertyIsBetween);
+        final PropertyIsBetween filter = (PropertyIsBetween) obj;
+        assertEquals(FF.between(FF.property("att"), FF.literal(15), FF.literal(30)), filter);
+    }
+
+    @Test
+    public void testIn() throws CQLException {
+        final String cql = "att IN ( 15, 30, 'hello')";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof Or);
+        final Or filter = (Or) obj;
+        assertEquals(FF.equals(FF.property("att"), FF.literal(15)), filter.getChildren().get(0));
+        assertEquals(FF.equals(FF.property("att"), FF.literal(30)), filter.getChildren().get(1));
+        assertEquals(FF.equals(FF.property("att"), FF.literal("hello")), filter.getChildren().get(2));
+    }
+
+    @Test
+    public void testNotIn() throws CQLException {
+        final String cql = "att NOT IN ( 15, 30, 'hello')";
+        Object obj = CQL.parseFilter(cql);
+        obj = ((Not)obj).getFilter();
+        assertTrue(obj instanceof Or);
+        final Or filter = (Or) obj;
+        assertEquals(FF.equals(FF.property("att"), FF.literal(15)), filter.getChildren().get(0));
+        assertEquals(FF.equals(FF.property("att"), FF.literal(30)), filter.getChildren().get(1));
+        assertEquals(FF.equals(FF.property("att"), FF.literal("hello")), filter.getChildren().get(2));
+    }
+
+    @Test
+    public void testPropertyIsEqualTo1() throws CQLException {
+        final String cql = "att=15";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof PropertyIsEqualTo);
+        final PropertyIsEqualTo filter = (PropertyIsEqualTo) obj;
+        assertEquals(FF.equals(FF.property("att"), FF.literal(15)), filter);
+    }
+
+    @Test
+    public void testPropertyIsEqualTo2() throws CQLException {
+        final String cql = "att = 15";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof PropertyIsEqualTo);
+        final PropertyIsEqualTo filter = (PropertyIsEqualTo) obj;
+        assertEquals(FF.equals(FF.property("att"), FF.literal(15)), filter);
+    }
+
+    @Test
+    public void testPropertyIsNotEqualTo() throws CQLException {
+        final String cql = "att <> 15";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof PropertyIsNotEqualTo);
+        final PropertyIsNotEqualTo filter = (PropertyIsNotEqualTo) obj;
+        assertEquals(FF.notEqual(FF.property("att"), FF.literal(15)), filter);
+    }
+
+    @Ignore
+    @Test
+    public void testPropertyIsNotEqualTo2() throws CQLException {
+        final String cql = "att <>'15'";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof PropertyIsNotEqualTo);
+        final PropertyIsNotEqualTo filter = (PropertyIsNotEqualTo) obj;
+        assertEquals(FF.notEqual(FF.property("att"), FF.literal(15)), filter);
+    }
+
+    @Test
+    public void testPropertyIsGreaterThan() throws CQLException {
+        final String cql = "att > 15";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof PropertyIsGreaterThan);
+        final PropertyIsGreaterThan filter = (PropertyIsGreaterThan) obj;
+        assertEquals(FF.greater(FF.property("att"), FF.literal(15)), filter);
+    }
+
+    @Test
+    public void testPropertyIsGreaterThanOrEqualTo() throws CQLException {
+        final String cql = "att >= 15";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof PropertyIsGreaterThanOrEqualTo);
+        final PropertyIsGreaterThanOrEqualTo filter = (PropertyIsGreaterThanOrEqualTo) obj;
+        assertEquals(FF.greaterOrEqual(FF.property("att"), FF.literal(15)), filter);
+    }
+
+    @Test
+    public void testPropertyIsLessThan() throws CQLException {
+        final String cql = "att < 15";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof PropertyIsLessThan);
+        final PropertyIsLessThan filter = (PropertyIsLessThan) obj;
+        assertEquals(FF.less(FF.property("att"), FF.literal(15)), filter);
+    }
+
+    @Test
+    public void testPropertyIsLessThanOrEqualTo() throws CQLException {
+        final String cql = "att <= 15";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof PropertyIsLessThanOrEqualTo);
+        final PropertyIsLessThanOrEqualTo filter = (PropertyIsLessThanOrEqualTo) obj;
+        assertEquals(FF.lessOrEqual(FF.property("att"), FF.literal(15)), filter);
+    }
+
+    @Ignore
+    @Test
+    public void testPropertyIsLike() throws CQLException {
+        final String cql = "att LIKE '%hello_'";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof PropertyIsLike);
+        final PropertyIsLike filter = (PropertyIsLike) obj;
+        assertEquals(FF.like(FF.property("att"),"%hello_", "%", "_", "\\",true), filter);
+    }
+
+    @Ignore
+    @Test
+    public void testPropertyIsNotLike() throws CQLException {
+        final String cql = "att NOT LIKE '%hello_'";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof Not);
+        final Not filter = (Not) obj;
+        assertEquals(FF.not(FF.like(FF.property("att"),"%hello_", "%", "_", "\\",true)), filter);
+    }
+
+    @Ignore
+    @Test
+    public void testPropertyIsLikeInsensitive() throws CQLException {
+        final String cql = "att ILIKE '%hello_'";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof PropertyIsLike);
+        final PropertyIsLike filter = (PropertyIsLike) obj;
+        assertEquals(FF.like(FF.property("att"),"%hello_", "%", "_", "\\",false), filter);
+    }
+
+    @Test
+    public void testPropertyIsNull() throws CQLException {
+        final String cql = "att IS NULL";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof PropertyIsNull);
+        final PropertyIsNull filter = (PropertyIsNull) obj;
+        assertEquals(FF.isNull(FF.property("att")), filter);
+    }
+
+    @Test
+    public void testPropertyIsNotNull() throws CQLException {
+        final String cql = "att IS NOT NULL";
+        Object obj = CQL.parseFilter(cql);
+        obj = ((Not)obj).getFilter();
+        assertTrue(obj instanceof PropertyIsNull);
+        final PropertyIsNull filter = (PropertyIsNull) obj;
+        assertEquals(FF.isNull(FF.property("att")), filter);
+    }
+
+    @Ignore
+    @Test
+    public void testBBOX1() throws CQLException {
+        final String cql = "BBOX(\"att\" ,10, 20, 30, 40)";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof BBOX);
+        final BinarySpatialOperator filter = (BBOX) obj;
+        assertEquals(FF.bbox(FF.property("att"), 10,20,30,40, null), filter);
+    }
+
+    @Ignore
+    @Test
+    public void testBBOX2() throws CQLException {
+        final String cql = "BBOX(\"att\" ,10, 20, 30, 40, 'CRS:84')";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof BBOX);
+        final BBOX filter = (BBOX) obj;
+        assertEquals(FF.bbox(FF.property("att"), 10,20,30,40, "CRS:84"), filter);
+    }
+
+    @Ignore
+    @Test
+    public void testBBOX3() throws CQLException {
+        final String cql = "BBOX(att ,10, 20, 30, 40, 'CRS:84')";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof BBOX);
+        final BBOX filter = (BBOX) obj;
+        assertEquals(FF.bbox(FF.property("att"), 10,20,30,40, "CRS:84"), filter);
+    }
+
+    @Ignore
+    @Test
+    public void testBBOX4() throws CQLException {
+        final String cql = "BBOX(geometry,-10,-20,10,20)";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof BBOX);
+        final BBOX filter = (BBOX) obj;
+        assertEquals(FF.bbox(FF.property("geometry"), -10,-20,10,20,null), filter);
+    }
+
+    @Ignore
+    @Test
+    public void testBeyond() throws CQLException {
+        final String cql = "BEYOND(\"att\" ,POLYGON((10 20, 30 40, 50 60, 10 20)), 10, meters)";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof Beyond);
+        final Beyond filter = (Beyond) obj;
+
+        assertEquals(FF.property("att"), filter.getExpression1());
+        assertTrue(filter.getExpression2() instanceof Literal);
+        assertTrue( ((Literal)filter.getExpression2()).getValue() instanceof Geometry);
+        final Geometry filtergeo = (Geometry) ((Literal)filter.getExpression2()).getValue();
+        assertTrue(baseGeometry.equalsExact(filtergeo));
+    }
+
+    @Ignore
+    @Test
+    public void testContains() throws CQLException {
+        final String cql = "CONTAINS(\"att\" ,POLYGON((10 20, 30 40, 50 60, 10 20)))";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof Contains);
+        final Contains filter = (Contains) obj;
+
+        assertEquals(FF.property("att"), filter.getExpression1());
+        assertTrue(filter.getExpression2() instanceof Literal);
+        assertTrue( ((Literal)filter.getExpression2()).getValue() instanceof Geometry);
+        final Geometry filtergeo = (Geometry) ((Literal)filter.getExpression2()).getValue();
+        assertTrue(baseGeometry.equalsExact(filtergeo));
+    }
+
+    @Ignore
+    @Test
+    public void testCrosses() throws CQLException {
+        final String cql = "CROSSES(\"att\" ,POLYGON((10 20, 30 40, 50 60, 10 20)))";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof Crosses);
+        final Crosses filter = (Crosses) obj;
+
+        assertEquals(FF.property("att"), filter.getExpression1());
+        assertTrue(filter.getExpression2() instanceof Literal);
+        assertTrue( ((Literal)filter.getExpression2()).getValue() instanceof Geometry);
+        final Geometry filtergeo = (Geometry) ((Literal)filter.getExpression2()).getValue();
+        assertTrue(baseGeometry.equalsExact(filtergeo));
+    }
+
+    @Ignore
+    @Test
+    public void testDisjoint() throws CQLException {
+        final String cql = "DISJOINT(\"att\" ,POLYGON((10 20, 30 40, 50 60, 10 20)))";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof Disjoint);
+        final Disjoint filter = (Disjoint) obj;
+
+        assertEquals(FF.property("att"), filter.getExpression1());
+        assertTrue(filter.getExpression2() instanceof Literal);
+        assertTrue( ((Literal)filter.getExpression2()).getValue() instanceof Geometry);
+        final Geometry filtergeo = (Geometry) ((Literal)filter.getExpression2()).getValue();
+        assertTrue(baseGeometry.equalsExact(filtergeo));
+    }
+
+    @Ignore
+    @Test
+    public void testDWithin() throws CQLException {
+        final String cql = "DWITHIN(\"att\" ,POLYGON((10 20, 30 40, 50 60, 10 20)), 10, 'meters')";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof DWithin);
+        final DWithin filter = (DWithin) obj;
+
+        assertEquals(FF.property("att"), filter.getExpression1());
+        assertEquals(10.0, filter.getDistance(), DELTA);
+        assertEquals("meters", filter.getDistanceUnits());
+        assertTrue(filter.getExpression2() instanceof Literal);
+        assertTrue( ((Literal)filter.getExpression2()).getValue() instanceof Geometry);
+        final Geometry filtergeo = (Geometry) ((Literal)filter.getExpression2()).getValue();
+        assertTrue(baseGeometry.equalsExact(filtergeo));
+    }
+
+    @Ignore
+    @Test
+    public void testDWithin2() throws CQLException {
+        //there is an error in this syntax, meters is a literal so it should be writen 'meters"
+        //but this writing is commun so we tolerate it
+        final String cql = "DWITHIN(BoundingBox, POINT(12.1 28.9), 10, meters)";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof DWithin);
+        final DWithin filter = (DWithin) obj;
+
+        assertEquals(FF.property("BoundingBox"), filter.getExpression1());
+        assertEquals(10.0, filter.getDistance(), DELTA);
+        assertEquals("meters", filter.getDistanceUnits());
+        assertTrue(filter.getExpression2() instanceof Literal);
+        assertTrue( ((Literal)filter.getExpression2()).getValue() instanceof Geometry);
+        final Geometry filtergeo = (Geometry) ((Literal)filter.getExpression2()).getValue();
+        assertTrue(baseGeometryPoint.equalsExact(filtergeo));
+
+    }
+
+    @Ignore
+    @Test
+    public void testEquals() throws CQLException {
+        final String cql = "EQUALS(\"att\" ,POLYGON((10 20, 30 40, 50 60, 10 20)))";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof Equals);
+        final Equals filter = (Equals) obj;
+
+        assertEquals(FF.property("att"), filter.getExpression1());
+        assertTrue(filter.getExpression2() instanceof Literal);
+        assertTrue( ((Literal)filter.getExpression2()).getValue() instanceof Geometry);
+        final Geometry filtergeo = (Geometry) ((Literal)filter.getExpression2()).getValue();
+        assertTrue(baseGeometry.equalsExact(filtergeo));
+    }
+
+    @Ignore
+    @Test
+    public void testIntersects() throws CQLException {
+        final String cql = "INTERSECTS(\"att\" ,POLYGON((10 20, 30 40, 50 60, 10 20)))";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof Intersects);
+        final Intersects filter = (Intersects) obj;
+
+        assertEquals(FF.property("att"), filter.getExpression1());
+        assertTrue(filter.getExpression2() instanceof Literal);
+        assertTrue( ((Literal)filter.getExpression2()).getValue() instanceof Geometry);
+        final Geometry filtergeo = (Geometry) ((Literal)filter.getExpression2()).getValue();
+        assertTrue(baseGeometry.equalsExact(filtergeo));
+    }
+
+    @Ignore
+    @Test
+    public void testOverlaps() throws CQLException {
+        final String cql = "OVERLAPS(\"att\" ,POLYGON((10 20, 30 40, 50 60, 10 20)))";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof Overlaps);
+        final Overlaps filter = (Overlaps) obj;
+
+        assertEquals(FF.property("att"), filter.getExpression1());
+        assertTrue(filter.getExpression2() instanceof Literal);
+        assertTrue( ((Literal)filter.getExpression2()).getValue() instanceof Geometry);
+        final Geometry filtergeo = (Geometry) ((Literal)filter.getExpression2()).getValue();
+        assertTrue(baseGeometry.equalsExact(filtergeo));
+    }
+
+    @Ignore
+    @Test
+    public void testTouches() throws CQLException {
+        final String cql = "TOUCHES(\"att\" ,POLYGON((10 20, 30 40, 50 60, 10 20)))";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof Touches);
+        final Touches filter = (Touches) obj;
+
+        assertEquals(FF.property("att"), filter.getExpression1());
+        assertTrue(filter.getExpression2() instanceof Literal);
+        assertTrue( ((Literal)filter.getExpression2()).getValue() instanceof Geometry);
+        final Geometry filtergeo = (Geometry) ((Literal)filter.getExpression2()).getValue();
+        assertTrue(baseGeometry.equalsExact(filtergeo));
+    }
+
+    @Ignore
+    @Test
+    public void testWithin() throws CQLException {
+        final String cql = "WITHIN(\"att\" ,POLYGON((10 20, 30 40, 50 60, 10 20)))";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof Within);
+        final Within filter = (Within) obj;
+
+        assertEquals(FF.property("att"), filter.getExpression1());
+        assertTrue(filter.getExpression2() instanceof Literal);
+        assertTrue( ((Literal)filter.getExpression2()).getValue() instanceof Geometry);
+        final Geometry filtergeo = (Geometry) ((Literal)filter.getExpression2()).getValue();
+        assertTrue(baseGeometry.equalsExact(filtergeo));
+    }
+
+    @Ignore
+    @Test
+    public void testCombine1() throws CQLException {
+        final String cql = "NOT att = 15 OR att BETWEEN 15 AND 30";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof Or);
+        final Or filter = (Or) obj;
+        assertEquals(
+                FF.or(
+                    FF.not(FF.equals(FF.property("att"), FF.literal(15))),
+                    FF.between(FF.property("att"), FF.literal(15), FF.literal(30))
+                ),
+                filter
+                );
+    }
+
+    @Ignore
+    @Test
+    public void testCombine2() throws CQLException {
+        final String cql = "(NOT att = 15) OR (att BETWEEN 15 AND 30)";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof Or);
+        final Or filter = (Or) obj;
+        assertEquals(
+                FF.or(
+                    FF.not(FF.equals(FF.property("att"), FF.literal(15))),
+                    FF.between(FF.property("att"), FF.literal(15), FF.literal(30))
+                ),
+                filter
+                );
+    }
+
+    @Ignore
+    @Test
+    public void testCombine3() throws CQLException {
+        final String cql = "(NOT att1 = 15) AND (att2 = 15 OR att3 BETWEEN 15 AND 30) AND (att4 BETWEEN 1 AND 2)";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof And);
+        final And filter = (And) obj;
+        assertEquals(
+                FF.and(
+                    UnmodifiableArrayList.wrap(new Filter[] {(Filter)
+                        FF.not(FF.equals(FF.property("att1"), FF.literal(15))),
+                        FF.or(
+                            FF.equals(FF.property("att2"), FF.literal(15)),
+                            FF.between(FF.property("att3"), FF.literal(15), FF.literal(30))
+                        ),
+                        FF.between(FF.property("att4"), FF.literal(1), FF.literal(2))
+                    })
+                ),
+                filter
+                );
+    }
+
+    @Test
+    public void testCombine4() throws CQLException {
+        final String cql = "(x+7) <= (y-9)";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof PropertyIsLessThanOrEqualTo);
+        final PropertyIsLessThanOrEqualTo filter = (PropertyIsLessThanOrEqualTo) obj;
+        assertEquals(
+                FF.lessOrEqual(
+                    FF.add(FF.property("x"), FF.literal(7)),
+                    FF.subtract(FF.property("y"), FF.literal(9))
+                ),
+                filter
+                );
+    }
+
+    @Ignore
+    @Test
+    public void testAfter() throws CQLException, ParseException {
+        final String cql = "att AFTER 2012-03-21T05:42:36Z";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof After);
+        final After filter = (After) obj;
+
+        assertEquals(FF.property("att"), filter.getExpression1());
+        assertTrue(filter.getExpression2() instanceof Literal);
+        assertTrue( ((Literal)filter.getExpression2()).getValue() instanceof Date);
+        final Date filterdate = (Date) ((Literal)filter.getExpression2()).getValue();
+        assertEquals(TemporalUtilities.parseDate("2012-03-21T05:42:36Z"), filterdate);
+    }
+
+    @Ignore
+    @Test
+    public void testAnyInteracts() throws CQLException, ParseException {
+        final String cql = "att ANYINTERACTS 2012-03-21T05:42:36Z";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof AnyInteracts);
+        final AnyInteracts filter = (AnyInteracts) obj;
+
+        assertEquals(FF.property("att"), filter.getExpression1());
+        assertTrue(filter.getExpression2() instanceof Literal);
+        assertTrue( ((Literal)filter.getExpression2()).getValue() instanceof Date);
+        final Date filterdate = (Date) ((Literal)filter.getExpression2()).getValue();
+        assertEquals(TemporalUtilities.parseDate("2012-03-21T05:42:36Z"), filterdate);
+    }
+
+    @Ignore
+    @Test
+    public void testBefore() throws CQLException, ParseException {
+        final String cql = "att BEFORE 2012-03-21T05:42:36Z";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof Before);
+        final Before filter = (Before) obj;
+
+        assertEquals(FF.property("att"), filter.getExpression1());
+        assertTrue(filter.getExpression2() instanceof Literal);
+        assertTrue( ((Literal)filter.getExpression2()).getValue() instanceof Date);
+        final Date filterdate = (Date) ((Literal)filter.getExpression2()).getValue();
+        assertEquals(TemporalUtilities.parseDate("2012-03-21T05:42:36Z"), filterdate);
+    }
+
+    @Ignore
+    @Test
+    public void testBegins() throws CQLException, ParseException {
+        final String cql = "att BEGINS 2012-03-21T05:42:36Z";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof Begins);
+        final Begins filter = (Begins) obj;
+
+        assertEquals(FF.property("att"), filter.getExpression1());
+        assertTrue(filter.getExpression2() instanceof Literal);
+        assertTrue( ((Literal)filter.getExpression2()).getValue() instanceof Date);
+        final Date filterdate = (Date) ((Literal)filter.getExpression2()).getValue();
+        assertEquals(TemporalUtilities.parseDate("2012-03-21T05:42:36Z"), filterdate);
+    }
+
+    @Ignore
+    @Test
+    public void testBegunBy() throws CQLException, ParseException {
+        final String cql = "att BEGUNBY 2012-03-21T05:42:36Z";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof BegunBy);
+        final BegunBy filter = (BegunBy) obj;
+
+        assertEquals(FF.property("att"), filter.getExpression1());
+        assertTrue(filter.getExpression2() instanceof Literal);
+        assertTrue( ((Literal)filter.getExpression2()).getValue() instanceof Date);
+        final Date filterdate = (Date) ((Literal)filter.getExpression2()).getValue();
+        assertEquals(TemporalUtilities.parseDate("2012-03-21T05:42:36Z"), filterdate);
+    }
+
+    @Ignore
+    @Test
+    public void testDuring() throws CQLException, ParseException {
+        final String cql = "att DURING 2012-03-21T05:42:36Z";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof During);
+        final During filter = (During) obj;
+
+        assertEquals(FF.property("att"), filter.getExpression1());
+        assertTrue(filter.getExpression2() instanceof Literal);
+        assertTrue( ((Literal)filter.getExpression2()).getValue() instanceof Date);
+        final Date filterdate = (Date) ((Literal)filter.getExpression2()).getValue();
+        assertEquals(TemporalUtilities.parseDate("2012-03-21T05:42:36Z"), filterdate);
+    }
+
+    @Ignore
+    @Test
+    public void testEndedBy() throws CQLException, ParseException {
+        final String cql = "att ENDEDBY 2012-03-21T05:42:36Z";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof EndedBy);
+        final EndedBy filter = (EndedBy) obj;
+
+        assertEquals(FF.property("att"), filter.getExpression1());
+        assertTrue(filter.getExpression2() instanceof Literal);
+        assertTrue( ((Literal)filter.getExpression2()).getValue() instanceof Date);
+        final Date filterdate = (Date) ((Literal)filter.getExpression2()).getValue();
+        assertEquals(TemporalUtilities.parseDate("2012-03-21T05:42:36Z"), filterdate);
+    }
+
+    @Ignore
+    @Test
+    public void testEnds() throws CQLException, ParseException {
+        final String cql = "att ENDS 2012-03-21T05:42:36Z";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof Ends);
+        final Ends filter = (Ends) obj;
+
+        assertEquals(FF.property("att"), filter.getExpression1());
+        assertTrue(filter.getExpression2() instanceof Literal);
+        assertTrue( ((Literal)filter.getExpression2()).getValue() instanceof Date);
+        final Date filterdate = (Date) ((Literal)filter.getExpression2()).getValue();
+        assertEquals(TemporalUtilities.parseDate("2012-03-21T05:42:36Z"), filterdate);
+    }
+
+    @Ignore
+    @Test
+    public void testMeets() throws CQLException, ParseException {
+        final String cql = "att MEETS 2012-03-21T05:42:36Z";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof Meets);
+        final Meets filter = (Meets) obj;
+
+        assertEquals(FF.property("att"), filter.getExpression1());
+        assertTrue(filter.getExpression2() instanceof Literal);
+        assertTrue( ((Literal)filter.getExpression2()).getValue() instanceof Date);
+        final Date filterdate = (Date) ((Literal)filter.getExpression2()).getValue();
+        assertEquals(TemporalUtilities.parseDate("2012-03-21T05:42:36Z"), filterdate);
+    }
+
+    @Ignore
+    @Test
+    public void testMetBy() throws CQLException, ParseException {
+        final String cql = "att METBY 2012-03-21T05:42:36Z";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof MetBy);
+        final MetBy filter = (MetBy) obj;
+
+        assertEquals(FF.property("att"), filter.getExpression1());
+        assertTrue(filter.getExpression2() instanceof Literal);
+        assertTrue( ((Literal)filter.getExpression2()).getValue() instanceof Date);
+        final Date filterdate = (Date) ((Literal)filter.getExpression2()).getValue();
+        assertEquals(TemporalUtilities.parseDate("2012-03-21T05:42:36Z"), filterdate);
+    }
+
+    @Ignore
+    @Test
+    public void testOverlappedBy() throws CQLException, ParseException {
+        final String cql = "att OVERLAPPEDBY 2012-03-21T05:42:36Z";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof OverlappedBy);
+        final OverlappedBy filter = (OverlappedBy) obj;
+
+        assertEquals(FF.property("att"), filter.getExpression1());
+        assertTrue(filter.getExpression2() instanceof Literal);
+        assertTrue( ((Literal)filter.getExpression2()).getValue() instanceof Date);
+        final Date filterdate = (Date) ((Literal)filter.getExpression2()).getValue();
+        assertEquals(TemporalUtilities.parseDate("2012-03-21T05:42:36Z"), filterdate);
+    }
+
+    @Ignore
+    @Test
+    public void testTcontains() throws CQLException, ParseException {
+        final String cql = "att TCONTAINS 2012-03-21T05:42:36Z";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof TContains);
+        final TContains filter = (TContains) obj;
+
+        assertEquals(FF.property("att"), filter.getExpression1());
+        assertTrue(filter.getExpression2() instanceof Literal);
+        assertTrue( ((Literal)filter.getExpression2()).getValue() instanceof Date);
+        final Date filterdate = (Date) ((Literal)filter.getExpression2()).getValue();
+        assertEquals(TemporalUtilities.parseDate("2012-03-21T05:42:36Z"), filterdate);
+    }
+
+    @Ignore
+    @Test
+    public void testTequals() throws CQLException, ParseException {
+        final String cql = "att TEQUALS 2012-03-21T05:42:36Z";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof TEquals);
+        final TEquals filter = (TEquals) obj;
+
+        assertEquals(FF.property("att"), filter.getExpression1());
+        assertTrue(filter.getExpression2() instanceof Literal);
+        assertTrue( ((Literal)filter.getExpression2()).getValue() instanceof Date);
+        final Date filterdate = (Date) ((Literal)filter.getExpression2()).getValue();
+        assertEquals(TemporalUtilities.parseDate("2012-03-21T05:42:36Z"), filterdate);
+    }
+
+    @Ignore
+    @Test
+    public void testToverlaps() throws CQLException, ParseException {
+        final String cql = "att TOVERLAPS 2012-03-21T05:42:36Z";
+        final Object obj = CQL.parseFilter(cql);
+        assertTrue(obj instanceof TOverlaps);
+        final TOverlaps filter = (TOverlaps) obj;
+
+        assertEquals(FF.property("att"), filter.getExpression1());
+        assertTrue(filter.getExpression2() instanceof Literal);
+        assertTrue( ((Literal)filter.getExpression2()).getValue() instanceof Date);
+        final Date filterdate = (Date) ((Literal)filter.getExpression2()).getValue();
+        assertEquals(TemporalUtilities.parseDate("2012-03-21T05:42:36Z"), filterdate);
+    }
+
+}
diff --git a/core/sis-cql/src/test/java/org/apache/sis/cql/FilterWritingTest.java b/core/sis-cql/src/test/java/org/apache/sis/cql/FilterWritingTest.java
new file mode 100644
index 0000000..26a671a
--- /dev/null
+++ b/core/sis-cql/src/test/java/org/apache/sis/cql/FilterWritingTest.java
@@ -0,0 +1,416 @@
+/*
+ * 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.sis.cql;
+
+import java.text.ParseException;
+import java.util.Collections;
+import org.apache.sis.filter.DefaultFilterFactory;
+import org.apache.sis.internal.util.UnmodifiableArrayList;
+import static org.junit.Assert.*;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.locationtech.jts.geom.Coordinate;
+import org.locationtech.jts.geom.Geometry;
+import org.locationtech.jts.geom.GeometryFactory;
+import org.locationtech.jts.geom.LinearRing;
+import org.opengis.filter.Filter;
+import org.opengis.filter.FilterFactory2;
+
+/**
+ * Test writing in CQL filters.
+ *
+ * @author  Johann Sorel (Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+public class FilterWritingTest {
+
+    private final FilterFactory2 FF = new DefaultFilterFactory();
+    private final GeometryFactory GF = new GeometryFactory();
+    private final Geometry baseGeometry = GF.createPolygon(
+                GF.createLinearRing(
+                    new Coordinate[]{
+                        new Coordinate(10, 20),
+                        new Coordinate(30, 40),
+                        new Coordinate(50, 60),
+                        new Coordinate(10, 20)
+                    }),
+                new LinearRing[0]
+                );
+
+    @Test
+    public void testExcludeFilter() throws CQLException {
+        final Filter filter = Filter.EXCLUDE;
+        final String cql = CQL.write(filter);
+        assertNotNull(cql);
+        assertEquals("1=0", cql);
+    }
+
+    @Test
+    public void testIncludeFilter() throws CQLException {
+        final Filter filter = Filter.INCLUDE;
+        final String cql = CQL.write(filter);
+        assertNotNull(cql);
+        assertEquals("1=1", cql);
+    }
+
+    @Test
+    public void testAnd() throws CQLException {
+        final Filter filter = FF.and(
+                UnmodifiableArrayList.wrap(new Filter[] {(Filter)
+                    FF.equals(FF.property("att1"), FF.literal(15)),
+                    FF.equals(FF.property("att2"), FF.literal(30)),
+                    FF.equals(FF.property("att3"), FF.literal(50))
+                }));
+        final String cql = CQL.write(filter);
+        assertNotNull(cql);
+        assertEquals("(\"att1\" = 15 AND \"att2\" = 30 AND \"att3\" = 50)", cql);
+    }
+
+    @Test
+    public void testOr() throws CQLException {
+        final Filter filter = FF.or(
+                UnmodifiableArrayList.wrap(new Filter[] {(Filter)
+                    FF.equals(FF.property("att1"), FF.literal(15)),
+                    FF.equals(FF.property("att2"), FF.literal(30)),
+                    FF.equals(FF.property("att3"), FF.literal(50))
+                }));
+        final String cql = CQL.write(filter);
+        assertNotNull(cql);
+        assertEquals("(\"att1\" = 15 OR \"att2\" = 30 OR \"att3\" = 50)", cql);
+    }
+
+    @Test
+    public void testId() throws CQLException {
+        final Filter filter = FF.id(Collections.singleton(FF.featureId("test-1")));
+        try{
+            final String cql = CQL.write(filter);
+            fail("ID filter does not exist in CQL");
+        }catch(UnsupportedOperationException ex){
+            //ok
+        }
+    }
+
+    @Test
+    public void testNot() throws CQLException {
+        final Filter filter = FF.not(FF.equals(FF.property("att"), FF.literal(15)));
+        final String cql = CQL.write(filter);
+        assertNotNull(cql);
+        assertEquals("NOT att = 15", cql);
+    }
+
+    @Ignore
+    @Test
+    public void testPropertyIsBetween() throws CQLException {
+        final Filter filter = FF.between(FF.property("att"), FF.literal(15), FF.literal(30));
+        final String cql = CQL.write(filter);
+        assertNotNull(cql);
+        assertEquals("att BETWEEN 15 AND 30", cql);
+    }
+
+    @Test
+    public void testPropertyIsEqualTo() throws CQLException {
+        final Filter filter = FF.equals(FF.property("att"), FF.literal(15));
+        final String cql = CQL.write(filter);
+        assertNotNull(cql);
+        assertEquals("att = 15", cql);
+    }
+
+    @Test
+    public void testPropertyIsNotEqualTo() throws CQLException {
+        final Filter filter = FF.notEqual(FF.property("att"), FF.literal(15));
+        final String cql = CQL.write(filter);
+        assertNotNull(cql);
+        assertEquals("att <> 15", cql);
+    }
+
+    @Test
+    public void testPropertyIsGreaterThan() throws CQLException {
+        final Filter filter = FF.greater(FF.property("att"), FF.literal(15));
+        final String cql = CQL.write(filter);
+        assertNotNull(cql);
+        assertEquals("att > 15", cql);
+    }
+
+    @Test
+    public void testPropertyIsGreaterThanOrEqualTo() throws CQLException {
+        final Filter filter = FF.greaterOrEqual(FF.property("att"), FF.literal(15));
+        final String cql = CQL.write(filter);
+        assertNotNull(cql);
+        assertEquals("att >= 15", cql);
+    }
+
+    @Test
+    public void testPropertyIsLessThan() throws CQLException {
+        final Filter filter = FF.less(FF.property("att"), FF.literal(15));
+        final String cql = CQL.write(filter);
+        assertNotNull(cql);
+        assertEquals("att < 15", cql);
+    }
+
+    @Test
+    public void testPropertyIsLessThanOrEqualTo() throws CQLException {
+        final Filter filter = FF.lessOrEqual(FF.property("att"), FF.literal(15));
+        final String cql = CQL.write(filter);
+        assertNotNull(cql);
+        assertEquals("att <= 15", cql);
+    }
+
+    @Ignore
+    @Test
+    public void testPropertyIsLike() throws CQLException {
+        final Filter filter = FF.like(FF.property("att"),"%hello");
+        final String cql = CQL.write(filter);
+        assertNotNull(cql);
+        assertEquals("att ILIKE '%hello'", cql);
+    }
+
+    @Test
+    public void testPropertyIsNull() throws CQLException {
+        final Filter filter = FF.isNull(FF.property("att"));
+        final String cql = CQL.write(filter);
+        assertNotNull(cql);
+        assertEquals("att IS NULL", cql);
+    }
+
+    @Ignore
+    @Test
+    public void testBBOX() throws CQLException {
+        final Filter filter = FF.bbox(FF.property("att"), 10,20,30,40, null);
+        final String cql = CQL.write(filter);
+        assertNotNull(cql);
+        assertEquals("BBOX(att,10.0,30.0,20.0,40.0)", cql);
+    }
+
+    @Ignore
+    @Test
+    public void testBeyond() throws CQLException {
+        final Filter filter = FF.beyond(FF.property("att"), FF.literal(baseGeometry), 0, "");
+        final String cql = CQL.write(filter);
+        assertNotNull(cql);
+        assertEquals("BEYOND(att,POLYGON ((10 20, 30 40, 50 60, 10 20)))", cql);
+    }
+
+    @Ignore
+    @Test
+    public void testContains() throws CQLException {
+        final Filter filter = FF.contains(FF.property("att"), FF.literal(baseGeometry));
+        final String cql = CQL.write(filter);
+        assertNotNull(cql);
+        assertEquals("CONTAINS(att,POLYGON ((10 20, 30 40, 50 60, 10 20)))", cql);
+    }
+
+    @Ignore
+    @Test
+    public void testCrosses() throws CQLException {
+        final Filter filter = FF.crosses(FF.property("att"), FF.literal(baseGeometry));
+        final String cql = CQL.write(filter);
+        assertNotNull(cql);
+        assertEquals("CROSSES(att,POLYGON ((10 20, 30 40, 50 60, 10 20)))", cql);
+    }
+
+    @Ignore
+    @Test
+    public void testDisjoint() throws CQLException {
+        final Filter filter = FF.disjoint(FF.property("att"), FF.literal(baseGeometry));
+        final String cql = CQL.write(filter);
+        assertNotNull(cql);
+        assertEquals("DISJOINT(att,POLYGON ((10 20, 30 40, 50 60, 10 20)))", cql);
+    }
+
+    @Ignore
+    @Test
+    public void testDWithin() throws CQLException {
+        final Filter filter = FF.dwithin(FF.property("att"), FF.literal(baseGeometry), 0, "");
+        final String cql = CQL.write(filter);
+        assertNotNull(cql);
+        assertEquals("DWITHIN(att,POLYGON ((10 20, 30 40, 50 60, 10 20)))", cql);
+    }
+
+    @Ignore
+    @Test
+    public void testEquals() throws CQLException {
+        final Filter filter = FF.equal(FF.property("att"), FF.literal(baseGeometry));
+        final String cql = CQL.write(filter);
+        assertNotNull(cql);
+        assertEquals("EQUALS(att,POLYGON ((10 20, 30 40, 50 60, 10 20)))", cql);
+    }
+
+    @Ignore
+    @Test
+    public void testIntersects() throws CQLException {
+        final Filter filter = FF.intersects(FF.property("att"), FF.literal(baseGeometry));
+        final String cql = CQL.write(filter);
+        assertNotNull(cql);
+        assertEquals("INTERSECTS(att,POLYGON ((10 20, 30 40, 50 60, 10 20)))", cql);
+    }
+
+    @Ignore
+    @Test
+    public void testOverlaps() throws CQLException {
+        final Filter filter = FF.overlaps(FF.property("att"), FF.literal(baseGeometry));
+        final String cql = CQL.write(filter);
+        assertNotNull(cql);
+        assertEquals("OVERLAPS(att,POLYGON ((10 20, 30 40, 50 60, 10 20)))", cql);
+    }
+
+    @Ignore
+    @Test
+    public void testTouches() throws CQLException {
+        final Filter filter = FF.touches(FF.property("att"), FF.literal(baseGeometry));
+        final String cql = CQL.write(filter);
+        assertNotNull(cql);
+        assertEquals("TOUCHES(att,POLYGON ((10 20, 30 40, 50 60, 10 20)))", cql);
+    }
+
+    @Ignore
+    @Test
+    public void testWithin() throws CQLException {
+        final Filter filter = FF.within(FF.property("att"), FF.literal(baseGeometry));
+        final String cql = CQL.write(filter);
+        assertNotNull(cql);
+        assertEquals("WITHIN(att,POLYGON ((10 20, 30 40, 50 60, 10 20)))", cql);
+    }
+
+    @Ignore
+    @Test
+    public void testAfter() throws CQLException, ParseException {
+        final Filter filter = FF.after(FF.property("att"), FF.literal(TemporalUtilities.parseDate("2012-03-21T05:42:36Z")));
+        final String cql = CQL.write(filter);
+        assertNotNull(cql);
+        assertEquals("att AFTER 2012-03-21T05:42:36Z", cql);
+    }
+
+    @Ignore
+    @Test
+    public void testAnyInteracts() throws CQLException, ParseException {
+        final Filter filter = FF.anyInteracts(FF.property("att"), FF.literal(TemporalUtilities.parseDate("2012-03-21T05:42:36Z")));
+        final String cql = CQL.write(filter);
+        assertNotNull(cql);
+        assertEquals("att ANYINTERACTS 2012-03-21T05:42:36Z", cql);
+    }
+
+    @Ignore
+    @Test
+    public void testBefore() throws CQLException, ParseException {
+        final Filter filter = FF.before(FF.property("att"), FF.literal(TemporalUtilities.parseDate("2012-03-21T05:42:36Z")));
+        final String cql = CQL.write(filter);
+        assertNotNull(cql);
+        assertEquals("att BEFORE 2012-03-21T05:42:36Z", cql);
+    }
+
+    @Ignore
+    @Test
+    public void testBegins() throws CQLException, ParseException {
+        final Filter filter = FF.begins(FF.property("att"), FF.literal(TemporalUtilities.parseDate("2012-03-21T05:42:36Z")));
+        final String cql = CQL.write(filter);
+        assertNotNull(cql);
+        assertEquals("att BEGINS 2012-03-21T05:42:36Z", cql);
+    }
+
+    @Ignore
+    @Test
+    public void testBegunBy() throws CQLException, ParseException {
+        final Filter filter = FF.begunBy(FF.property("att"), FF.literal(TemporalUtilities.parseDate("2012-03-21T05:42:36Z")));
+        final String cql = CQL.write(filter);
+        assertNotNull(cql);
+        assertEquals("att BEGUNBY 2012-03-21T05:42:36Z", cql);
+    }
+
+    @Ignore
+    @Test
+    public void testDuring() throws CQLException, ParseException {
+        final Filter filter = FF.during(FF.property("att"), FF.literal(TemporalUtilities.parseDate("2012-03-21T05:42:36Z")));
+        final String cql = CQL.write(filter);
+        assertNotNull(cql);
+        assertEquals("att DURING 2012-03-21T05:42:36Z", cql);
+    }
+
+    @Ignore
+    @Test
+    public void testEndedBy() throws CQLException, ParseException {
+        final Filter filter = FF.endedBy(FF.property("att"), FF.literal(TemporalUtilities.parseDate("2012-03-21T05:42:36Z")));
+        final String cql = CQL.write(filter);
+        assertNotNull(cql);
+        assertEquals("att ENDEDBY 2012-03-21T05:42:36Z", cql);
+    }
+
+    @Ignore
+    @Test
+    public void testEnds() throws CQLException, ParseException {
+        final Filter filter = FF.ends(FF.property("att"), FF.literal(TemporalUtilities.parseDate("2012-03-21T05:42:36Z")));
+        final String cql = CQL.write(filter);
+        assertNotNull(cql);
+        assertEquals("att ENDS 2012-03-21T05:42:36Z", cql);
+    }
+
+    @Ignore
+    @Test
+    public void testMeets() throws CQLException, ParseException {
+        final Filter filter = FF.meets(FF.property("att"), FF.literal(TemporalUtilities.parseDate("2012-03-21T05:42:36Z")));
+        final String cql = CQL.write(filter);
+        assertNotNull(cql);
+        assertEquals("att MEETS 2012-03-21T05:42:36Z", cql);
+    }
+
+    @Ignore
+    @Test
+    public void testMetBy() throws CQLException, ParseException {
+        final Filter filter = FF.metBy(FF.property("att"), FF.literal(TemporalUtilities.parseDate("2012-03-21T05:42:36Z")));
+        final String cql = CQL.write(filter);
+        assertNotNull(cql);
+        assertEquals("att METBY 2012-03-21T05:42:36Z", cql);
+    }
+
+    @Ignore
+    @Test
+    public void testOverlappedBy() throws CQLException, ParseException {
+        final Filter filter = FF.overlappedBy(FF.property("att"), FF.literal(TemporalUtilities.parseDate("2012-03-21T05:42:36Z")));
+        final String cql = CQL.write(filter);
+        assertNotNull(cql);
+        assertEquals("att OVERLAPPEDBY 2012-03-21T05:42:36Z", cql);
+    }
+
+    @Ignore
+    @Test
+    public void testTcontains() throws CQLException, ParseException {
+        final Filter filter = FF.tcontains(FF.property("att"), FF.literal(TemporalUtilities.parseDate("2012-03-21T05:42:36Z")));
+        final String cql = CQL.write(filter);
+        assertNotNull(cql);
+        assertEquals("att TCONTAINS 2012-03-21T05:42:36Z", cql);
+    }
+
+    @Ignore
+    @Test
+    public void testTequals() throws CQLException, ParseException {
+        final Filter filter = FF.tequals(FF.property("att"), FF.literal(TemporalUtilities.parseDate("2012-03-21T05:42:36Z")));
+        final String cql = CQL.write(filter);
+        assertNotNull(cql);
+        assertEquals("att TEQUALS 2012-03-21T05:42:36Z", cql);
+    }
+
+    @Ignore
+    @Test
+    public void testToverlaps() throws CQLException, ParseException {
+        final Filter filter = FF.toverlaps(FF.property("att"), FF.literal(TemporalUtilities.parseDate("2012-03-21T05:42:36Z")));
+        final String cql = CQL.write(filter);
+        assertNotNull(cql);
+        assertEquals("att TOVERLAPS 2012-03-21T05:42:36Z", cql);
+    }
+
+}
diff --git a/core/sis-cql/src/test/java/org/apache/sis/cql/TemporalUtilities.java b/core/sis-cql/src/test/java/org/apache/sis/cql/TemporalUtilities.java
new file mode 100644
index 0000000..158ed14
--- /dev/null
+++ b/core/sis-cql/src/test/java/org/apache/sis/cql/TemporalUtilities.java
@@ -0,0 +1,34 @@
+/*
+ * 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.sis.cql;
+
+import java.util.Date;
+
+/**
+ *
+ * @author  Johann Sorel (Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+class TemporalUtilities {
+
+    static Date parseDate(String date) {
+        throw new UnsupportedOperationException("todo");
+    }
+
+}


Mime
View raw message