Coverage for gws-app/gws/plugin/ows_server/csw/__init__.py: 81%
108 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-16 23:09 +0200
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-16 23:09 +0200
1"""CSW service.
3Basic implementation of the OGC Catalogue Service for the Web (CSW) standard.
4Only a small subset of features is supported.
6References:
7 - OpenGIS Catalogue Service Implementation Specification 2.0.2 (http://portal.opengeospatial.org/files/?artifact_id=20555)
8"""
10from typing import cast
11import gws
12import gws.base.metadata
13import gws.base.map
14import gws.base.ows.server as server
15import gws.base.search.filter
16import gws.base.shape
17import gws.config.util
18import gws.lib.crs
19import gws.lib.datetimex
20import gws.lib.extent
21import gws.lib.mime
22import gws.lib.uom
24gws.ext.new.owsService('csw')
26_cdir = gws.u.dirname(__file__)
28_DEFAULT_TEMPLATES_ISO = [
29 gws.Config(
30 type='py',
31 path=f'{_cdir}/templates/iso/getCapabilities.cx.py',
32 subject='ows.GetCapabilities',
33 mimeTypes=[gws.lib.mime.XML],
34 ),
35 gws.Config(
36 type='py',
37 path=f'{_cdir}/templates/iso/describeRecord.cx.py',
38 subject='ows.DescribeRecord',
39 mimeTypes=[gws.lib.mime.XML],
40 ),
41 gws.Config(
42 type='py',
43 path=f'{_cdir}/templates/iso/getRecords.cx.py',
44 subject='ows.GetRecords',
45 mimeTypes=[gws.lib.mime.XML],
46 ),
47 gws.Config(
48 type='py',
49 path=f'{_cdir}/templates/iso/getRecords.cx.py',
50 subject='ows.GetRecordById',
51 mimeTypes=[gws.lib.mime.XML],
52 ),
53]
55_DEFAULT_METADATA = dict(
56 name='CSW',
57 inspireMandatoryKeyword='infoMapAccessService',
58 inspireDegreeOfConformity='notEvaluated',
59 inspireResourceType='service',
60 inspireSpatialDataServiceType='view',
61 isoScope='dataset',
62 isoServiceFunction='download',
63 isoSpatialRepresentationType='vector',
64)
67class Profile(gws.Enum):
68 """Metadata profile for CSW service."""
70 ISO = 'ISO'
71 """ISO 19115 metadata profile."""
72 DCMI = 'DCMI'
73 """Dublin Core metadata profile."""
76class Config(server.service.Config):
77 """CSW Service configuration"""
79 profile: Profile = Profile.ISO
80 """Metadata profile."""
83class Object(server.service.Object):
84 protocol = gws.OwsProtocol.CSW
85 supportedVersions = ['2.0.2']
87 mdMap: dict[str, gws.Metadata]
88 profile: Profile
90 def configure(self):
91 self.mdMap = {}
92 self.profile = Profile.ISO
94 def configure_templates(self):
95 extra = _DEFAULT_TEMPLATES_ISO
96 return gws.config.util.configure_templates_for(self, extra=extra)
98 def configure_metadata(self):
99 super().configure_metadata()
100 self.metadata = gws.base.metadata.from_args(_DEFAULT_METADATA, self.metadata)
102 def configure_operations(self):
103 self.supportedOperations = [
104 gws.OwsOperation(
105 verb=gws.OwsVerb.GetCapabilities,
106 formats=[gws.lib.mime.XML],
107 handlerName='handle_get_capabilities',
108 ),
109 gws.OwsOperation(
110 verb=gws.OwsVerb.DescribeRecord,
111 formats=[gws.lib.mime.XML],
112 handlerName='handle_describe_record',
113 ),
114 gws.OwsOperation(
115 verb=gws.OwsVerb.GetRecords,
116 formats=[gws.lib.mime.XML],
117 handlerName='handle_get_records',
118 ),
119 gws.OwsOperation(
120 verb=gws.OwsVerb.GetRecordById,
121 formats=[gws.lib.mime.XML],
122 handlerName='handle_get_record_by_id',
123 ),
124 ]
126 def post_configure(self):
127 self.collect_metadata()
129 ##
131 def parse_xml_request(self, xml):
132 params = {}
134 params['REQUEST'] = xml.name
135 return params
137 ##
139 def init_request(self, req):
140 sr = super().init_request(req)
141 sr.load_project()
142 return sr
144 def handle_get_capabilities(self, sr: server.request.Object):
145 return self.template_response(sr)
147 def handle_describe_record(self, sr: server.request.Object):
148 return self.template_response(sr)
150 def handle_get_records(self, sr: server.request.Object):
151 mds = self._find_metas(sr)
153 mdc = server.MetadataCollection(
154 members=mds,
155 numMatched=len(mds),
156 numReturned=len(mds),
157 timestamp=gws.lib.datetimex.to_iso_string(with_tz=':'),
158 )
160 return self.template_response(
161 sr,
162 '',
163 metadataCollection=mdc,
164 next=0,
165 )
167 def handle_get_record_by_id(self, sr: server.request.Object):
168 md = self._find_meta_by_id(sr)
169 mds = [md] if md else []
171 mdc = server.MetadataCollection(
172 members=mds,
173 numMatched=len(mds),
174 numReturned=len(mds),
175 timestamp=gws.lib.datetimex.to_iso_string(with_tz=':'),
176 )
178 return self.template_response(
179 sr,
180 '',
181 metadataCollection=mdc,
182 next=0,
183 )
185 ##
187 def collect_metadata(self):
188 # collect objects whose metadata should be published in the catalog
189 #
190 # - object should have `metadata`
191 # - object must be public
192 # - `metadata` should have `catalogUid`
193 # - `metadata.metaLinks` should be empty
194 #
195 # `metadata.metaLinks[0]` will be set to our csw url
197 self.mdMap = {}
199 for obj in self.root.find_all():
200 self._collect_metadata_from_object(obj)
202 gws.log.info(f'CSW: configured with {len(self.mdMap)} records')
204 def _collect_metadata_from_object(self, obj: gws.Node):
205 md: gws.Metadata = cast(gws.Metadata, gws.u.get(obj, 'metadata'))
207 if not md:
208 return
210 if not md.get('catalogUid'):
211 # gws.log.debug(f'CSW: skip {obj.uid}: no catalogUid')
212 return
214 cid = gws.u.to_uid(md.get('catalogUid'))
216 if not self.root.app.authMgr.is_public_object(obj):
217 gws.log.debug(f'CSW: skip {obj.uid}: not public')
218 return
220 extra = {}
222 extra['catalogUid'] = cid
223 extra['catalogCitationUid'] = md.get('catalogCitationUid') or cid
224 extra['metaLinks'] = list(md.get('metaLinks') or [])
225 extra['metaLinks'].append(self._make_link(cid))
227 extent = gws.u.get(obj, 'extent') or gws.u.get(obj, 'map.extent')
228 crs = gws.u.get(obj, 'crs') or gws.u.get(obj, 'map.crs')
229 if extent and crs:
230 extra['wgsExtent'] = gws.lib.extent.transform_to_wgs(extent, crs)
231 extra['crs'] = crs
232 # @TODO get boundingPolygonElement somehow
234 map = obj.find_closest(gws.ext.object.map)
235 if map:
236 extra['isoSpatialResolution'] = gws.lib.uom.res_to_scale(cast(gws.base.map.Object, map).initResolution)
238 self.mdMap[cid] = gws.base.metadata.from_args(md, extra)
240 ##
242 def _make_link(self, cid):
243 return gws.MetadataLink(
244 url=gws.u.action_url_path('owsService', serviceUid=self.uid, request='GetRecordById', id=cid),
245 format=gws.lib.mime.XML,
246 type='TC211' if self.profile == 'ISO' else 'DCMI',
247 function='download',
248 )
250 def _find_metas(self, sr: server.request.Object):
251 flt_el = None
252 if sr.xmlElement:
253 flt_el = sr.xmlElement.findfirst('Query/Constraint/Filter')
255 if not flt_el:
256 return self.mdMap.values()
258 flt = gws.base.search.filter.from_fes_element(flt_el)
259 m = gws.base.search.filter.Matcher()
261 return [md for md in self.mdMap.values() if m.matches(flt, md)]
263 def _find_meta_by_id(self, sr: server.request.Object):
264 for md in self.mdMap.values():
265 if md.catalogUid == sr.req.param('id'):
266 return md