1 /***********************************************************************
2  * Copyright (c) 2013-2024 Commonwealth Computer Research, Inc.
3  * All rights reserved. This program and the accompanying materials
4  * are made available under the terms of the Apache License, Version 2.0
5  * which accompanies this distribution and is available at
6  * http://www.opensource.org/licenses/apache2.0.php.
7  ***********************************************************************/
8 
9 package org.locationtech.geomesa.convert2.transforms
10 
11 import com.typesafe.scalalogging.LazyLogging
12 import org.geotools.api.filter.expression.PropertyName
13 import org.geotools.factory.CommonFactoryFinder
14 import org.geotools.filter.expression.{PropertyAccessor, PropertyAccessorFactory}
15 import org.geotools.util.factory.Hints
16 import org.locationtech.geomesa.convert2.transforms.CqlFunctionFactory.{CqlTransformerFunction, arrayIndexProperty}
17 import org.locationtech.geomesa.convert2.transforms.Expression.Literal
18 import org.locationtech.geomesa.convert2.transforms.TransformerFunction.NamedTransformerFunction
19 
20 class CqlFunctionFactory extends TransformerFunctionFactory with LazyLogging {
21 
22   import scala.collection.JavaConverters._
23 
24   override val functions: Seq[TransformerFunction] = {
25     val builder = Seq.newBuilder[TransformerFunction]
26 
27     CommonFactoryFinder.getFunctionFactories(null).asScala.toSeq.foreach { factory =>
28       val names = try {
29         // only import default cql functions (without namespaces)
30         // exclude 'categorize', as it doesn't parse into a function correctly
31         factory.getFunctionNames.asScala.filter(f => f.getFunctionName.getNamespaceURI == null && f.getName != "Categorize")
32       } catch {
33         // if CQL classes aren't on the classpath, these functions won't be available
34         case e: NoClassDefFoundError =>
35           logger.warn(s"Couldn't create cql function factory '${factory.getClass.getName}': ${e.toString}")
36           Seq.empty
37       }
38       names.foreach { f =>
39         val name = f.getFunctionName.toString
40         val expressions = Array.tabulate(f.getArguments.size())(arrayIndexProperty)
41         try { builder += new CqlTransformerFunction(name, expressions) } catch {
42           case e: Exception => logger.warn(s"Couldn't create cql function '$name': ${e.toString}")
43         }
44       }
45     }
46     builder.result()
47   }
48 }
49 
50 object CqlFunctionFactory {
51 
52   private val ff = CommonFactoryFinder.getFilterFactory
53 
54   // use alphas for the array indices, as used by the ArrayPropertyAccessor, below
55   private def arrayIndexProperty(i: Int): PropertyName = ff.property(('a' + i).toChar.toString)
56 
57   class CqlTransformerFunction(name: String, expressions: Array[_ <: org.geotools.api.filter.expression.Expression])
58       extends NamedTransformerFunction(Seq(s"cql:$name")) {
59 
60     private val fn = ff.function(name, expressions: _*)
61 
62     override def apply(args: Array[AnyRef]): AnyRef = fn.evaluate(args)
63 
64     override def getInstance(args: List[Expression]): CqlTransformerFunction = {
65       // remap literals to cql literals so that they can be optimized (e.g. prepared geometries, etc)
66       val expressions = Array.tabulate(fn.getFunctionName.getArguments.size()) { i =>
67         if (i < args.length && args(i).isInstanceOf[Literal[_]]) {
68           ff.literal(args(i).asInstanceOf[Literal[_]].value)
69         } else {
70           arrayIndexProperty(i)
71         }
72       }
73       new CqlTransformerFunction(name, expressions)
74     }
75   }
76 
77   /**
78     * For accessing 'properties' of arrays (instead of simple features)
79     */
80   class ArrayPropertyAccessorFactory extends PropertyAccessorFactory {
81     override def createPropertyAccessor(typ: Class[_],
82                                         xpath: String,
83                                         target: Class[_],
84                                         hints: Hints): PropertyAccessor = {
85       if (typ.isArray) { new ArrayPropertyAccessor } else { null }
86     }
87   }
88 
89   /**
90     * Accessing properties of an array. Indices are expected to be lower-case letters,
91     * where 'a' indicates the first element, b the second, etc. We use alphas because
92     * integers are interpreted as literals, not properties.
93     */
94   class ArrayPropertyAccessor extends PropertyAccessor {
95 
96     override def canHandle(obj: Any, xpath: String, target: Class[_]): Boolean = {
97       val i = toIndex(xpath)
98       i >= 0 && i < obj.asInstanceOf[Array[Any]].length
99     }
100 
101     override def set[T](obj: Any, xpath: String, value: T, target: Class[T]): Unit =
102       obj.asInstanceOf[Array[Any]].update(toIndex(xpath), value)
103 
104     override def get[T](obj: Any, xpath: String, target: Class[T]): T =
105       obj.asInstanceOf[Array[Any]].apply(toIndex(xpath)).asInstanceOf[T]
106 
107     private def toIndex(xpath: String): Int = {
108       if (xpath.length == 1) {
109         xpath.charAt(0) - 'a'
110       } else {
111         -1
112       }
113     }
114   }
115 }
Line Stmt Id Pos Tree Symbol Tests Code
25 2119 1270 - 1305 TypeApply scala.collection.Seq.newBuilder scala.collection.Seq.newBuilder[org.locationtech.geomesa.convert2.transforms.TransformerFunction]
27 2120 1311 - 1357 Apply org.geotools.factory.CommonFactoryFinder.getFunctionFactories org.geotools.factory.CommonFactoryFinder.getFunctionFactories(null)
27 2138 1311 - 2325 Apply scala.collection.IterableLike.foreach scala.collection.JavaConverters.asScalaSetConverter[org.geotools.filter.FunctionFactory](org.geotools.factory.CommonFactoryFinder.getFunctionFactories(null)).asScala.toSeq.foreach[Unit](((factory: org.geotools.filter.FunctionFactory) => { val names: Seq[org.geotools.api.filter.capability.FunctionName] = try { scala.collection.JavaConverters.asScalaBufferConverter[org.geotools.api.filter.capability.FunctionName](factory.getFunctionNames()).asScala.filter(((f: org.geotools.api.filter.capability.FunctionName) => f.getFunctionName().getNamespaceURI().==(null).&&(f.getName().!=("Categorize")))) } catch { case (e @ (_: NoClassDefFoundError)) => { (if (CqlFunctionFactory.this.logger.underlying.isWarnEnabled()) CqlFunctionFactory.this.logger.underlying.warn("Couldn\'t create cql function factory \'{}\': {}", (scala.Array.apply[AnyRef]((factory.getClass().getName(): AnyRef), (e.toString(): AnyRef))((ClassTag.AnyRef: scala.reflect.ClassTag[AnyRef])): _*)) else (): Unit); scala.collection.Seq.empty[Nothing] } }; names.foreach[Any](((f: org.geotools.api.filter.capability.FunctionName) => { val name: String = f.getFunctionName().toString(); val expressions: Array[org.geotools.api.filter.expression.PropertyName] = scala.Array.tabulate[org.geotools.api.filter.expression.PropertyName](f.getArguments().size())({ ((i: Int) => org.locationtech.geomesa.convert2.transforms.CqlFunctionFactory.arrayIndexProperty(i)) })((ClassTag.apply[org.geotools.api.filter.expression.PropertyName](classOf[org.geotools.api.filter.expression.PropertyName]): scala.reflect.ClassTag[org.geotools.api.filter.expression.PropertyName])); try { builder.+=(new org.locationtech.geomesa.convert2.transforms.CqlFunctionFactory.CqlTransformerFunction(name, expressions)) } catch { case (e @ (_: Exception)) => (if (CqlFunctionFactory.this.logger.underlying.isWarnEnabled()) CqlFunctionFactory.this.logger.underlying.warn("Couldn\'t create cql function \'{}\': {}", (scala.Array.apply[AnyRef]((name: AnyRef), (e.toString(): AnyRef))((ClassTag.AnyRef: scala.reflect.ClassTag[AnyRef])): _*)) else (): Unit) } })) }))
31 2121 1570 - 1594 Apply org.geotools.filter.FunctionFactory.getFunctionNames factory.getFunctionNames()
31 2122 1652 - 1656 Literal <nosymbol> null
31 2123 1660 - 1685 Apply java.lang.Object.!= f.getName().!=("Categorize")
31 2124 1615 - 1685 Apply scala.Boolean.&& f.getFunctionName().getNamespaceURI().==(null).&&(f.getName().!=("Categorize"))
31 2125 1570 - 1686 Apply scala.collection.TraversableLike.filter scala.collection.JavaConverters.asScalaBufferConverter[org.geotools.api.filter.capability.FunctionName](factory.getFunctionNames()).asScala.filter(((f: org.geotools.api.filter.capability.FunctionName) => f.getFunctionName().getNamespaceURI().==(null).&&(f.getName().!=("Categorize"))))
31 2126 1570 - 1686 Block scala.collection.TraversableLike.filter scala.collection.JavaConverters.asScalaBufferConverter[org.geotools.api.filter.capability.FunctionName](factory.getFunctionNames()).asScala.filter(((f: org.geotools.api.filter.capability.FunctionName) => f.getFunctionName().getNamespaceURI().==(null).&&(f.getName().!=("Categorize"))))
34 2128 1826 - 1956 Block <nosymbol> { (if (CqlFunctionFactory.this.logger.underlying.isWarnEnabled()) CqlFunctionFactory.this.logger.underlying.warn("Couldn\'t create cql function factory \'{}\': {}", (scala.Array.apply[AnyRef]((factory.getClass().getName(): AnyRef), (e.toString(): AnyRef))((ClassTag.AnyRef: scala.reflect.ClassTag[AnyRef])): _*)) else (): Unit); scala.collection.Seq.empty[Nothing] }
36 2127 1947 - 1956 TypeApply scala.collection.generic.GenericCompanion.empty scala.collection.Seq.empty[Nothing]
38 2137 1971 - 2319 Apply scala.collection.IterableLike.foreach names.foreach[Any](((f: org.geotools.api.filter.capability.FunctionName) => { val name: String = f.getFunctionName().toString(); val expressions: Array[org.geotools.api.filter.expression.PropertyName] = scala.Array.tabulate[org.geotools.api.filter.expression.PropertyName](f.getArguments().size())({ ((i: Int) => org.locationtech.geomesa.convert2.transforms.CqlFunctionFactory.arrayIndexProperty(i)) })((ClassTag.apply[org.geotools.api.filter.expression.PropertyName](classOf[org.geotools.api.filter.expression.PropertyName]): scala.reflect.ClassTag[org.geotools.api.filter.expression.PropertyName])); try { builder.+=(new org.locationtech.geomesa.convert2.transforms.CqlFunctionFactory.CqlTransformerFunction(name, expressions)) } catch { case (e @ (_: Exception)) => (if (CqlFunctionFactory.this.logger.underlying.isWarnEnabled()) CqlFunctionFactory.this.logger.underlying.warn("Couldn\'t create cql function \'{}\': {}", (scala.Array.apply[AnyRef]((name: AnyRef), (e.toString(): AnyRef))((ClassTag.AnyRef: scala.reflect.ClassTag[AnyRef])): _*)) else (): Unit) } }))
39 2129 2011 - 2037 Apply java.lang.Object.toString f.getFunctionName().toString()
40 2130 2079 - 2100 Apply java.util.List.size f.getArguments().size()
40 2131 2102 - 2120 Apply org.locationtech.geomesa.convert2.transforms.CqlFunctionFactory.arrayIndexProperty org.locationtech.geomesa.convert2.transforms.CqlFunctionFactory.arrayIndexProperty(i)
40 2132 2064 - 2121 ApplyToImplicitArgs scala.Array.tabulate scala.Array.tabulate[org.geotools.api.filter.expression.PropertyName](f.getArguments().size())({ ((i: Int) => org.locationtech.geomesa.convert2.transforms.CqlFunctionFactory.arrayIndexProperty(i)) })((ClassTag.apply[org.geotools.api.filter.expression.PropertyName](classOf[org.geotools.api.filter.expression.PropertyName]): scala.reflect.ClassTag[org.geotools.api.filter.expression.PropertyName]))
41 2133 2147 - 2192 Apply org.locationtech.geomesa.convert2.transforms.CqlFunctionFactory.CqlTransformerFunction.<init> new org.locationtech.geomesa.convert2.transforms.CqlFunctionFactory.CqlTransformerFunction(name, expressions)
41 2134 2136 - 2192 Apply scala.collection.mutable.Builder.+= builder.+=(new org.locationtech.geomesa.convert2.transforms.CqlFunctionFactory.CqlTransformerFunction(name, expressions))
41 2135 2136 - 2192 Block scala.collection.mutable.Builder.+= builder.+=(new org.locationtech.geomesa.convert2.transforms.CqlFunctionFactory.CqlTransformerFunction(name, expressions))
42 2136 2234 - 2301 Typed <nosymbol> (if (CqlFunctionFactory.this.logger.underlying.isWarnEnabled()) CqlFunctionFactory.this.logger.underlying.warn("Couldn\'t create cql function \'{}\': {}", (scala.Array.apply[AnyRef]((name: AnyRef), (e.toString(): AnyRef))((ClassTag.AnyRef: scala.reflect.ClassTag[AnyRef])): _*)) else (): Unit)
46 2139 2330 - 2346 Apply scala.collection.mutable.Builder.result builder.result()
52 2140 2402 - 2438 Apply org.geotools.factory.CommonFactoryFinder.getFilterFactory org.geotools.factory.CommonFactoryFinder.getFilterFactory()
55 2141 2592 - 2617 Apply scala.Any.toString 'a'.+(i).toChar.toString()
55 2142 2580 - 2618 Apply org.geotools.api.filter.FilterFactory.property CqlFunctionFactory.this.ff.property('a'.+(i).toChar.toString())
60 2143 2831 - 2835 Select org.locationtech.geomesa.convert2.transforms.CqlFunctionFactory.CqlTransformerFunction.name CqlTransformerFunction.this.name
60 2144 2837 - 2848 Select org.locationtech.geomesa.convert2.transforms.CqlFunctionFactory.CqlTransformerFunction.expressions CqlTransformerFunction.this.expressions
60 2145 2819 - 2853 Apply org.geotools.api.filter.FilterFactory.function CqlFunctionFactory.this.ff.function(CqlTransformerFunction.this.name, (CqlTransformerFunction.this.expressions: _*))
62 2146 2909 - 2926 Apply org.geotools.api.filter.expression.Expression.evaluate CqlTransformerFunction.this.fn.evaluate(args)
66 2147 3150 - 3188 Apply java.util.List.size CqlTransformerFunction.this.fn.getFunctionName().getArguments().size()
66 2156 3135 - 3391 ApplyToImplicitArgs scala.Array.tabulate scala.Array.tabulate[org.geotools.api.filter.expression.Expression](CqlTransformerFunction.this.fn.getFunctionName().getArguments().size())(((i: Int) => if (i.<(args.length).&&(args.apply(i).isInstanceOf[org.locationtech.geomesa.convert2.transforms.Expression.Literal[_]])) CqlFunctionFactory.this.ff.literal(args.apply(i).asInstanceOf[org.locationtech.geomesa.convert2.transforms.Expression.Literal[_]].value) else CqlFunctionFactory.this.arrayIndexProperty(i)))((ClassTag.apply[org.geotools.api.filter.expression.Expression](classOf[org.geotools.api.filter.expression.Expression]): scala.reflect.ClassTag[org.geotools.api.filter.expression.Expression]))
67 2148 3213 - 3224 Select scala.collection.LinearSeqOptimized.length args.length
67 2149 3228 - 3260 TypeApply scala.Any.isInstanceOf args.apply(i).isInstanceOf[org.locationtech.geomesa.convert2.transforms.Expression.Literal[_]]
67 2150 3209 - 3260 Apply scala.Boolean.&& i.<(args.length).&&(args.apply(i).isInstanceOf[org.locationtech.geomesa.convert2.transforms.Expression.Literal[_]])
68 2151 3285 - 3323 Select org.locationtech.geomesa.convert2.transforms.Expression.Literal.value args.apply(i).asInstanceOf[org.locationtech.geomesa.convert2.transforms.Expression.Literal[_]].value
68 2152 3274 - 3324 Apply org.geotools.api.filter.FilterFactory.literal CqlFunctionFactory.this.ff.literal(args.apply(i).asInstanceOf[org.locationtech.geomesa.convert2.transforms.Expression.Literal[_]].value)
68 2153 3274 - 3324 Block org.geotools.api.filter.FilterFactory.literal CqlFunctionFactory.this.ff.literal(args.apply(i).asInstanceOf[org.locationtech.geomesa.convert2.transforms.Expression.Literal[_]].value)
70 2154 3352 - 3373 Apply org.locationtech.geomesa.convert2.transforms.CqlFunctionFactory.arrayIndexProperty CqlFunctionFactory.this.arrayIndexProperty(i)
70 2155 3352 - 3373 Block org.locationtech.geomesa.convert2.transforms.CqlFunctionFactory.arrayIndexProperty CqlFunctionFactory.this.arrayIndexProperty(i)
73 2157 3425 - 3429 Select org.locationtech.geomesa.convert2.transforms.CqlFunctionFactory.CqlTransformerFunction.name CqlTransformerFunction.this.name
73 2158 3398 - 3443 Apply org.locationtech.geomesa.convert2.transforms.CqlFunctionFactory.CqlTransformerFunction.<init> new CqlFunctionFactory.this.CqlTransformerFunction(CqlTransformerFunction.this.name, expressions)
85 2159 3865 - 3876 Apply java.lang.Class.isArray typ.isArray()
85 2160 3880 - 3905 Apply org.locationtech.geomesa.convert2.transforms.CqlFunctionFactory.ArrayPropertyAccessor.<init> new CqlFunctionFactory.this.ArrayPropertyAccessor()
85 2161 3880 - 3905 Block org.locationtech.geomesa.convert2.transforms.CqlFunctionFactory.ArrayPropertyAccessor.<init> new CqlFunctionFactory.this.ArrayPropertyAccessor()
85 2162 3915 - 3919 Literal <nosymbol> null
85 2163 3915 - 3919 Block <nosymbol> null
97 2164 4334 - 4348 Apply org.locationtech.geomesa.convert2.transforms.CqlFunctionFactory.ArrayPropertyAccessor.toIndex ArrayPropertyAccessor.this.toIndex(xpath)
98 2165 4360 - 4361 Literal <nosymbol> 0
98 2166 4369 - 4404 Select scala.Array.length obj.asInstanceOf[Array[Any]].length
98 2167 4365 - 4404 Apply scala.Int.< i.<(obj.asInstanceOf[Array[Any]].length)
98 2168 4355 - 4404 Apply scala.Boolean.&& i.>=(0).&&(i.<(obj.asInstanceOf[Array[Any]].length))
102 2169 4539 - 4553 Apply org.locationtech.geomesa.convert2.transforms.CqlFunctionFactory.ArrayPropertyAccessor.toIndex ArrayPropertyAccessor.this.toIndex(xpath)
102 2170 4503 - 4561 Apply scala.Array.update obj.asInstanceOf[Array[Any]].update(ArrayPropertyAccessor.this.toIndex(xpath), value)
105 2171 4676 - 4690 Apply org.locationtech.geomesa.convert2.transforms.CqlFunctionFactory.ArrayPropertyAccessor.toIndex ArrayPropertyAccessor.this.toIndex(xpath)
105 2172 4641 - 4707 TypeApply scala.Any.asInstanceOf obj.asInstanceOf[Array[Any]].apply(ArrayPropertyAccessor.this.toIndex(xpath)).asInstanceOf[T]
108 2173 4767 - 4784 Apply scala.Int.== xpath.length().==(1)
109 2174 4796 - 4817 Apply scala.Char.- xpath.charAt(0).-('a')
109 2175 4796 - 4817 Block scala.Char.- xpath.charAt(0).-('a')
111 2176 4841 - 4843 Literal <nosymbol> -1
111 2177 4841 - 4843 Block <nosymbol> -1