gt:             Expression s1 = getParameter(function, 0, true);
context: /*
   *    GeoTools - The Open Source Java GIS Toolkit
   *    http://geotools.org
   *
   *    (C) 2002-2018, Open Source Geospatial Foundation (OSGeo)
   *
   *    This library is free software; you can redistribute it and/or
   *    modify it under the terms of the GNU Lesser General Public
   *    License as published by the Free Software Foundation;
   *    version 2.1 of the License.
   *
   *    This library is distributed in the hope that it will be useful,
   *    but WITHOUT ANY WARRANTY; without even the implied warranty of
   *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   *    Lesser General Public License for more details.
   */
  package org.geotools.data.postgis;
  
  import java.io.IOException;
  import java.io.StringWriter;
  import java.io.Writer;
  import java.math.BigDecimal;
  import java.math.BigInteger;
  import java.sql.Date;
  import java.sql.Time;
  import java.sql.Timestamp;
  import java.util.ArrayList;
  import java.util.List;
  import java.util.concurrent.TimeUnit;
  import java.util.function.BiConsumer;
  import java.util.stream.Collectors;
  import java.util.stream.Stream;
  import org.apache.commons.lang3.math.NumberUtils;
  import org.geotools.data.jdbc.FilterToSQL;
  import org.geotools.data.postgis.filter.FilterFunction_pgNearest;
  import org.geotools.factory.CommonFactoryFinder;
  import org.geotools.filter.FilterCapabilities;
  import org.geotools.filter.LengthFunction;
  import org.geotools.filter.LiteralExpressionImpl;
  import org.geotools.filter.function.DateDifferenceFunction;
  import org.geotools.filter.function.FilterFunction_area;
  import org.geotools.filter.function.FilterFunction_buffer;
  import org.geotools.filter.function.FilterFunction_equalTo;
  import org.geotools.filter.function.FilterFunction_strConcat;
  import org.geotools.filter.function.FilterFunction_strEndsWith;
  import org.geotools.filter.function.FilterFunction_strEqualsIgnoreCase;
  import org.geotools.filter.function.FilterFunction_strIndexOf;
  import org.geotools.filter.function.FilterFunction_strLength;
  import org.geotools.filter.function.FilterFunction_strReplace;
  import org.geotools.filter.function.FilterFunction_strStartsWith;
  import org.geotools.filter.function.FilterFunction_strSubstring;
  import org.geotools.filter.function.FilterFunction_strSubstringStart;
  import org.geotools.filter.function.FilterFunction_strToLowerCase;
  import org.geotools.filter.function.FilterFunction_strToUpperCase;
  import org.geotools.filter.function.FilterFunction_strTrim;
  import org.geotools.filter.function.FilterFunction_strTrim2;
  import org.geotools.filter.function.InArrayFunction;
  import org.geotools.filter.function.JsonPointerFunction;
  import org.geotools.filter.function.math.FilterFunction_abs;
  import org.geotools.filter.function.math.FilterFunction_abs_2;
  import org.geotools.filter.function.math.FilterFunction_abs_3;
  import org.geotools.filter.function.math.FilterFunction_abs_4;
  import org.geotools.filter.function.math.FilterFunction_ceil;
  import org.geotools.filter.function.math.FilterFunction_floor;
  import org.geotools.geometry.jts.JTS;
  import org.geotools.jdbc.JDBCDataStore;
  import org.geotools.jdbc.PreparedFilterToSQL;
  import org.geotools.jdbc.PrimaryKeyColumn;
  import org.geotools.jdbc.SQLDialect;
  import org.geotools.util.factory.Hints;
  import org.locationtech.jts.geom.Envelope;
  import org.locationtech.jts.geom.Geometry;
  import org.locationtech.jts.geom.GeometryComponentFilter;
  import org.locationtech.jts.geom.GeometryFactory;
  import org.locationtech.jts.geom.MultiPolygon;
  import org.locationtech.jts.geom.Polygon;
  import org.opengis.feature.type.AttributeDescriptor;
  import org.opengis.feature.type.GeometryDescriptor;
  import org.opengis.filter.BinaryComparisonOperator;
  import org.opengis.filter.MultiValuedFilter;
  import org.opengis.filter.MultiValuedFilter.MatchAction;
  import org.opengis.filter.NativeFilter;
  import org.opengis.filter.PropertyIsBetween;
  import org.opengis.filter.PropertyIsEqualTo;
  import org.opengis.filter.expression.BinaryExpression;
  import org.opengis.filter.expression.Expression;
  import org.opengis.filter.expression.Function;
  import org.opengis.filter.expression.Literal;
  import org.opengis.filter.expression.NilExpression;
  import org.opengis.filter.expression.PropertyName;
  import org.opengis.filter.spatial.BBOX;
  import org.opengis.filter.spatial.BBOX3D;
  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.DistanceBufferOperator;
  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.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.TEquals;
  import org.opengis.filter.temporal.TOverlaps;
  import org.opengis.geometry.BoundingBox3D;
  
  class FilterToSqlHelper {
  
      protected static final String IO_ERROR = "io problem writing filter";
  
      private static final Envelope WORLD = new Envelope(-180, 180, -90, 90);
  
      FilterToSQL delegate;
      Writer out;
      boolean looseBBOXEnabled;
      boolean encodeBBOXFilterAsEnvelope;
  
      public FilterToSqlHelper(FilterToSQL delegate) {
          this.delegate = delegate;
      }
  
      public static FilterCapabilities createFilterCapabilities(boolean encodeFunctions) {
          FilterCapabilities caps = new FilterCapabilities();
          caps.addAll(SQLDialect.BASE_DBMS_CAPABILITIES);
  
          // adding the spatial filters support
          caps.addType(BBOX.class);
          caps.addType(BBOX3D.class);
          caps.addType(Contains.class);
          caps.addType(Crosses.class);
          caps.addType(Disjoint.class);
          caps.addType(Equals.class);
          caps.addType(Intersects.class);
          caps.addType(Overlaps.class);
          caps.addType(Touches.class);
          caps.addType(Within.class);
          caps.addType(DWithin.class);
          caps.addType(Beyond.class);
  
          // temporal filters
          caps.addType(After.class);
          caps.addType(Before.class);
          caps.addType(Begins.class);
          caps.addType(BegunBy.class);
          caps.addType(During.class);
          caps.addType(TOverlaps.class);
          caps.addType(Ends.class);
          caps.addType(EndedBy.class);
          caps.addType(TEquals.class);
  
          // replacement for area function that was in deprecated dialect registerFunction
          caps.addType(FilterFunction_area.class);
  
          if (encodeFunctions) {
              // add support for string functions
              caps.addType(FilterFunction_strConcat.class);
              caps.addType(FilterFunction_strEndsWith.class);
              caps.addType(FilterFunction_strStartsWith.class);
              caps.addType(FilterFunction_strEqualsIgnoreCase.class);
              caps.addType(FilterFunction_strIndexOf.class);
              caps.addType(FilterFunction_strLength.class);
              caps.addType(LengthFunction.class);
              caps.addType(FilterFunction_strToLowerCase.class);
              caps.addType(FilterFunction_strToUpperCase.class);
              caps.addType(FilterFunction_strReplace.class);
              caps.addType(FilterFunction_strSubstring.class);
              caps.addType(FilterFunction_strSubstringStart.class);
              caps.addType(FilterFunction_strTrim.class);
              caps.addType(FilterFunction_strTrim2.class);
  
              // add support for math functions
              caps.addType(FilterFunction_abs.class);
              caps.addType(FilterFunction_abs_2.class);
              caps.addType(FilterFunction_abs_3.class);
              caps.addType(FilterFunction_abs_4.class);
              caps.addType(FilterFunction_ceil.class);
              caps.addType(FilterFunction_floor.class);
  
              // time related functions
              caps.addType(DateDifferenceFunction.class);
  
              // n nearest function
              caps.addType(FilterFunction_pgNearest.class);
  
              // array functions
              caps.addType(InArrayFunction.class);
  
              // compare functions
              caps.addType(FilterFunction_equalTo.class);
  
              // one geometry function (to support testing, but otherwise fully functional)
              caps.addType(FilterFunction_buffer.class);
          }
          // native filter support
          caps.addType(NativeFilter.class);
  
          return caps;
      }
  
      protected Object visitBinarySpatialOperator(
              BinarySpatialOperator filter,
              PropertyName property,
              Literal geometry,
              boolean swapped,
              Object extraData) {
          try {
              if (filter instanceof DistanceBufferOperator) {
                  visitDistanceSpatialOperator(
                          (DistanceBufferOperator) filter, property, geometry, swapped, extraData);
              } else {
                  visitComparisonSpatialOperator(filter, property, geometry, swapped, extraData);
              }
          } catch (IOException e) {
              throw new RuntimeException(IO_ERROR, e);
          }
          return extraData;
      }
  
      protected Object visitBinarySpatialOperator(
              BinarySpatialOperator filter, Expression e1, Expression e2, Object extraData) {
  
          try {
              visitBinarySpatialOperator(filter, e1, e2, false, extraData);
          } catch (IOException e) {
              throw new RuntimeException(IO_ERROR, e);
          }
          return extraData;
      }
  
      void visitDistanceSpatialOperator(
              DistanceBufferOperator filter,
              PropertyName property,
              Literal geometry,
              boolean swapped,
              Object extraData)
              throws IOException {
          if ((filter instanceof DWithin && !swapped) || (filter instanceof Beyond && swapped)) {
              out.write("ST_DWithin(");
              property.accept(delegate, extraData);
              out.write(",");
              geometry.accept(delegate, extraData);
              out.write(",");
              out.write(toNativeUnits(filter));
              out.write(")");
          }
          if ((filter instanceof DWithin && swapped) || (filter instanceof Beyond && !swapped)) {
              out.write("ST_Distance(");
              property.accept(delegate, extraData);
              out.write(",");
              geometry.accept(delegate, extraData);
              out.write(") > ");
              out.write(toNativeUnits(filter));
          }
      }
  
      private String toNativeUnits(DistanceBufferOperator operator) {
          double distance;
          if (isCurrentGeography()) {
              // need the value in meters
              if (delegate instanceof PostgisPSFilterToSql) {
                  distance = ((PostgisPSFilterToSql) delegate).getDistanceInMeters(operator);
              } else {
                  distance = ((PostgisFilterToSQL) delegate).getDistanceInMeters(operator);
              }
          } else {
              // need the value in native units
              if (delegate instanceof PostgisPSFilterToSql) {
                  distance = ((PostgisPSFilterToSql) delegate).getDistanceInNativeUnits(operator);
              } else {
                  distance = ((PostgisFilterToSQL) delegate).getDistanceInNativeUnits(operator);
              }
          }
  
          return String.valueOf(distance);
      }
  
      void visitComparisonSpatialOperator(
              BinarySpatialOperator filter,
              PropertyName property,
              Literal geometry,
              boolean swapped,
              Object extraData)
              throws IOException {
  
          // if geography case, sanitize geometry first
          if (isCurrentGeography()) {
              geometry = clipToWorld(geometry);
              if (isWorld(geometry)) {
                  // nothing to filter in this case
                  out.write(" TRUE ");
                  return;
              } else if (isEmpty(geometry)) {
                  if (!(filter instanceof Disjoint)) {
                      out.write(" FALSE ");
                  } else {
                      out.write(" TRUE ");
                  }
                  return;
              }
          }
  
          // special case for 3D bbox, it requires a "weird" encoding, from a postgis mail
          // No, just construct a 3D geometry as your query filter, not a 3d box.
          // select count(*) from "3dfloor" where geom &&& ST_Makeline(ST_MakePoint(0,0,0),
          // ST_MakePoint(1000000,1000000,1));
          if (filter instanceof BBOX3D) {
              property.accept(delegate, extraData);
              out.write(" &&& ");
              BBOX3D bbox = (BBOX3D) filter;
              BoundingBox3D bounds = bbox.getBounds();
              out.write("ST_Makeline(ST_MakePoint(");
              out.write(bounds.getMinX() + "," + bounds.getMinY() + "," + bounds.getMinZ());
              out.write("), ST_MakePoint(");
              out.write(bounds.getMaxX() + "," + bounds.getMaxY() + "," + bounds.getMaxZ());
              out.write("))");
          } else {
              // add && filter if possible
              if (!(filter instanceof Disjoint)) {
                  if (encodeBBOXFilterAsEnvelope && !isCurrentGeography()) {
                      out.write("ST_envelope(");
                  }
                  property.accept(delegate, extraData);
                  if (encodeBBOXFilterAsEnvelope && !isCurrentGeography()) {
                      out.write(")");
                  }
                  out.write(" && ");
                  geometry.accept(delegate, extraData);
  
                  // if we're just encoding a bbox in loose mode, we're done
                  if (filter instanceof BBOX && looseBBOXEnabled) {
                      return;
                  }
  
                  out.write(" AND ");
              }
  
              visitBinarySpatialOperator(filter, property, (Expression) geometry, swapped, extraData);
          }
      }
  
      void visitBinarySpatialOperator(
              BinarySpatialOperator filter,
              Expression e1,
              Expression e2,
              boolean swapped,
              Object extraData)
              throws IOException {
  
          String closingParenthesis = ")";
          if (filter instanceof Equals) {
              out.write("ST_Equals");
          } else if (filter instanceof Disjoint) {
              out.write("NOT (ST_Intersects");
              closingParenthesis += ")";
          } else if (filter instanceof Intersects || filter instanceof BBOX) {
              out.write("ST_Intersects");
          } else if (filter instanceof Crosses) {
              out.write("ST_Crosses");
          } else if (filter instanceof Within) {
              if (swapped) out.write("ST_Contains");
              else out.write("ST_Within");
          } else if (filter instanceof Contains) {
              if (swapped) out.write("ST_Within");
              else out.write("ST_Contains");
          } else if (filter instanceof Overlaps) {
              out.write("ST_Overlaps");
          } else if (filter instanceof Touches) {
              out.write("ST_Touches");
          } else {
              throw new RuntimeException("Unsupported filter type " + filter.getClass());
          }
          out.write("(");
  
          e1.accept(delegate, extraData);
          out.write(", ");
          e2.accept(delegate, extraData);
  
          out.write(closingParenthesis);
      }
  
      boolean isCurrentGeography() {
          AttributeDescriptor geom = null;
          if (delegate instanceof PostgisPSFilterToSql) {
              geom = ((PostgisPSFilterToSql) delegate).getCurrentGeometry();
          } else if (delegate instanceof PostgisFilterToSQL) {
              geom = ((PostgisFilterToSQL) delegate).getCurrentGeometry();
          }
  
          return geom != null
                  && "geography".equals(geom.getUserData().get(JDBCDataStore.JDBC_NATIVE_TYPENAME));
      }
  
      private Literal clipToWorld(Literal geometry) {
          if (geometry != null) {
              Geometry g = geometry.evaluate(null, Geometry.class);
              if (g != null) {
                  Envelope env = g.getEnvelopeInternal();
                  // first, limit to world
                  if (!WORLD.contains(env)) {
                      g = sanitizePolygons(g.intersection(JTS.toGeometry(WORLD)));
                  }
  
                  // second, postgis will always use the shortest distance between two
                  // points, if an arc is longer than 180 degrees the opposite will
                  // be used instead, so we have to slice the geometry in parts
                  env = g.getEnvelopeInternal();
                  if (Math.sqrt(env.getWidth() * env.getWidth() + env.getHeight() * env.getHeight())
                          >= 180) {
                      // slice in 90x90 degrees quadrants, none of them has a diagonal longer than 180
                      final List<Polygon> polygons = new ArrayList<>();
                      for (double lon = Math.floor(env.getMinX()); lon < env.getMaxX(); lon += 90) {
                          for (double lat = Math.floor(env.getMinY());
                                  lat < env.getMaxY();
                                  lat += 90) {
                              Geometry quadrant =
                                      JTS.toGeometry(new Envelope(lon, lon + 90, lat, lat + 90));
                              Geometry cut = sanitizePolygons(g.intersection(quadrant));
                              if (!cut.isEmpty()) {
                                  if (cut instanceof Polygon) {
                                      polygons.add((Polygon) cut);
                                  } else {
                                      for (int i = 0; i < cut.getNumGeometries(); i++) {
                                          polygons.add((Polygon) cut.getGeometryN(i));
                                      }
                                  }
                              }
                          }
                      }
                      g = toPolygon(g.getFactory(), polygons);
                  }
  
                  geometry = CommonFactoryFinder.getFilterFactory(null).literal(g);
              }
          }
  
          return geometry;
      }
  
      /**
       * Given a geometry that might contain heterogeneous components extracts only the polygonal ones
       */
      private Geometry sanitizePolygons(Geometry geometry) {
          // already sane?
          if (geometry == null || geometry instanceof Polygon || geometry instanceof MultiPolygon) {
              return geometry;
          }
  
          // filter out only polygonal parts
          final List<Polygon> polygons = new ArrayList<>();
          geometry.apply(
                  (GeometryComponentFilter)
                          geom -> {
                              if (geom instanceof Polygon) {
                                  polygons.add((Polygon) geom);
                              }
                          });
  
          // turn filtered selection into a geometry
          return toPolygon(geometry.getFactory(), polygons);
      }
  
      private Geometry toPolygon(GeometryFactory gf, final List<Polygon> polygons) {
          if (polygons.isEmpty()) {
              return gf.createGeometryCollection(null);
          } else if (polygons.size() == 1) {
              return polygons.get(0);
          } else {
              return gf.createMultiPolygon(polygons.toArray(new Polygon[polygons.size()]));
          }
      }
  
      /** Returns true if the geometry covers the entire world */
      private boolean isWorld(Literal geometry) {
          if (geometry != null) {
              Geometry g = geometry.evaluate(null, Geometry.class);
              if (g != null) {
                  return JTS.toGeometry(WORLD).equalsTopo(g.union());
              }
          }
          return false;
      }
  
      /** Returns true if the geometry is fully empty */
      private boolean isEmpty(Literal geometry) {
          if (geometry != null) {
              Geometry g = geometry.evaluate(null, Geometry.class);
              return g == null || g.isEmpty();
          }
          return false;
      }
  
      /** Maps a function to its native db equivalent */
      public String getFunctionName(Function function) {
          if (function instanceof FilterFunction_strLength || function instanceof LengthFunction) {
              return "char_length";
          } else if (function instanceof FilterFunction_strToLowerCase) {
              return "lower";
          } else if (function instanceof FilterFunction_strToUpperCase) {
              return "upper";
          } else if (function instanceof FilterFunction_abs
                  || function instanceof FilterFunction_abs_2
                  || function instanceof FilterFunction_abs_3
                  || function instanceof FilterFunction_abs_4) {
              return "abs";
          }
          return function.getName();
      }
  
      /**
       * Performs custom visits for functions that cannot be encoded as <code>
       * functionName(p1, p2, ... pN).</code>
       */
      public boolean visitFunction(Function function, Object extraData) throws IOException {
          if (function instanceof DateDifferenceFunction) {
              Expression d1 = getParameter(function, 0, true);
              Expression d2 = getParameter(function, 1, true);
  
              List<Expression> params = function.getParameters();
              // extract epoch returns seconds, DateDifference can be defined in
              // a different time unit instead (ms as default).
              double multiplyingFactor = 1000;
              if (params.size() == 3) {
                  Expression expression = getParameter(function, 2, false);
                  if (expression instanceof Literal) {
                      TimeUnit timeUnit = expression.evaluate(null, TimeUnit.class);
                      if (timeUnit != TimeUnit.MILLISECONDS) {
                          // Let's identify the multiplying factor to go from seconds to the
                          // target time unit.
                          // Doing an inverse math since convert will return 0 when converting
                          // smaller units (i.e. 1 second) to bigger units (i.e. days)
                          multiplyingFactor = 1d / TimeUnit.SECONDS.convert(1, timeUnit);
                      }
                  }
              }
  
              out.write("(extract(epoch from ");
              d1.accept(delegate, java.util.Date.class);
              out.write("::timestamp - ");
              d2.accept(delegate, java.util.Date.class);
              out.write(") * " + multiplyingFactor + ")");
          } else if (function instanceof FilterFunction_area) {
  