Skip to content

Commit 553e82d

Browse files
authored
Merge pull request #146 from iboates/sql-view-params
Sql view params
2 parents 1ab51d0 + bd25f34 commit 553e82d

File tree

5 files changed

+323
-37
lines changed

5 files changed

+323
-37
lines changed

geo/Geoserver.py

+48-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# inbuilt libraries
22
import os
3-
from typing import List, Optional, Set, Union
3+
from typing import List, Optional, Set, Union, Dict, Iterable
44
from pathlib import Path
55

66
# third-party libraries
@@ -10,7 +10,7 @@
1010
# custom functions
1111
from .Calculation_gdal import raster_value
1212
from .Style import catagorize_xml, classified_xml, coverage_style_xml, outline_only_xml
13-
from .supports import prepare_zip_file, is_valid_xml
13+
from .supports import prepare_zip_file, is_valid_xml, is_surrounded_by_quotes
1414

1515

1616
# Custom exceptions.
@@ -2025,25 +2025,48 @@ def publish_featurestore_sqlview(
20252025
name: str,
20262026
store_name: str,
20272027
sql: str,
2028+
parameters: Optional[Iterable[Dict]] = None,
20282029
key_column: Optional[str] = None,
20292030
geom_name: str = "geom",
20302031
geom_type: str = "Geometry",
20312032
srid: Optional[int] = 4326,
20322033
workspace: Optional[str] = None,
20332034
):
20342035
"""
2036+
Publishes an SQL query as a layer, optionally with parameters
20352037
20362038
Parameters
20372039
----------
20382040
name : str
20392041
store_name : str
20402042
sql : str
2043+
parameters : iterable of dicts, optional
20412044
key_column : str, optional
20422045
geom_name : str, optional
20432046
geom_type : str, optional
20442047
workspace : str, optional
20452048
2049+
Notes
2050+
-----
2051+
With regards to SQL view parameters, it is advised to read the relevant section from the geoserver docs:
2052+
https://docs.geoserver.org/main/en/user/data/database/sqlview.html#parameterizing-sql-views
2053+
2054+
An integer-based parameter must have a default value
2055+
2056+
You should be VERY careful with the `regexp_validator`, as it can open you to SQL injection attacks. If you do
2057+
not supply one for a parameter, it will use the geoserver default `^[\w\d\s]+$`.
2058+
2059+
The `parameters` iterable must contain dictionaries with this structure:
2060+
2061+
```
2062+
{
2063+
"name": "<name of parameter (required)>"
2064+
"rexegpValidator": "<string containing regex validator> (optional)"
2065+
"defaultValue" : "<default value of parameter if not specified (required only for non-string parameters)>"
2066+
}
2067+
```
20462068
"""
2069+
20472070
if workspace is None:
20482071
workspace = "default"
20492072

@@ -2054,6 +2077,27 @@ def publish_featurestore_sqlview(
20542077
else:
20552078
key_column_xml = """"""
20562079

2080+
parameters_xml = ""
2081+
if parameters is not None:
2082+
for parameter in parameters:
2083+
2084+
# non-string parameters MUST have a default value supplied
2085+
if not is_surrounded_by_quotes(sql, parameter["name"]) and not "defaultValue" in parameter:
2086+
raise ValueError(f"Parameter `{parameter['name']}` appears to be a non-string in the supplied query"
2087+
", but does not have a default value specified. You must supply a default value "
2088+
"for non-string parameters using the `defaultValue` key.")
2089+
2090+
param_name = parameter.get("name", "")
2091+
default_value = parameter.get("defaultValue", "")
2092+
regexp_validator = parameter.get("regexpValidator", r"^[\w\d\s]+$")
2093+
parameters_xml += (f"""
2094+
<parameter>
2095+
<name>{param_name}</name>
2096+
<defaultValue>{default_value}</defaultValue>
2097+
<regexpValidator>{regexp_validator}</regexpValidator>
2098+
</parameter>\n
2099+
""".strip())
2100+
20572101
layer_xml = """<featureType>
20582102
<name>{0}</name>
20592103
<enabled>true</enabled>
@@ -2073,11 +2117,12 @@ def publish_featurestore_sqlview(
20732117
<type>{3}</type>
20742118
<srid>{5}</srid>
20752119
</geometry>{6}
2120+
{7}
20762121
</virtualTable>
20772122
</entry>
20782123
</metadata>
20792124
</featureType>""".format(
2080-
name, sql, geom_name, geom_type, workspace, srid, key_column_xml
2125+
name, sql, geom_name, geom_type, workspace, srid, key_column_xml, parameters_xml
20812126
)
20822127

20832128
# rest API url

geo/supports.py

+14
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
import re
23
from tempfile import mkstemp
34
from typing import Dict
45
from zipfile import ZipFile
@@ -60,3 +61,16 @@ def is_valid_xml(xml_string: str) -> bool:
6061
return True
6162
except ET.ParseError:
6263
return False
64+
65+
66+
def is_surrounded_by_quotes(text, param):
67+
# The regex pattern searches for '%foo%' surrounded by single quotes.
68+
# It uses \'%foo%\' to match '%foo%' literally, including the single quotes.
69+
pattern = rf"\'%{param}%\'"
70+
71+
# re.search() searches the string for the first location where the regex pattern produces a match.
72+
# If a match is found, re.search() returns a match object. Otherwise, it returns None.
73+
match = re.search(pattern, text)
74+
75+
# Return True if a match is found, False otherwise.
76+
return bool(match)

requirements_dev.txt

+3-1
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,6 @@ flake8
44
sphinx>=1.7
55
pre-commit
66
environs
7-
ddt
7+
ddt
8+
sqlalchemy>=2.0.29
9+
psycopg2>=2.9.9

tests/common.py

+17-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,21 @@
22

33
from geo.Geoserver import Geoserver
44

5-
GEO_URL = os.getenv("GEO_URL", "http://localhost:8080/geoserver")
5+
GEO_URL = os.getenv("GEO_URL", "http://localhost:8080/geoserver") # relative to test machine
66

7-
geo = Geoserver(GEO_URL, username="admin", password="geoserver")
7+
geo = Geoserver(GEO_URL, username=os.getenv("GEO_USER", "admin"), password=os.getenv("GEO_PASS", "geoserver"))
8+
9+
postgis_params = {
10+
"host": os.getenv("DB_HOST", "localhost"), # relative to the geoserver instance
11+
"port": os.getenv("DB_PORT", "5432"), # relative to the geoserver instance
12+
"db": os.getenv("DB_NAME", "geodb"),
13+
"pg_user": os.getenv("DB_USER", "geodb_user"),
14+
"pg_password": os.getenv("DB_PASS", "geodb_pass")
15+
}
16+
17+
# in case you are using docker or something, and the location of the database is different relative to your host machine
18+
postgis_params_local_override = {
19+
"host": os.getenv("DB_HOST_LOCAL", "localhost"), # relative to the test machine
20+
"port": os.getenv("DB_PORT_LOCAL", "5432"), # relative to the test machine
21+
}
22+
postgis_params_local = {**postgis_params, **postgis_params_local_override}

0 commit comments

Comments
 (0)