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 22:59 +0200

1"""CSW service. 

2 

3Basic implementation of the OGC Catalogue Service for the Web (CSW) standard. 

4Only a small subset of features is supported. 

5 

6References: 

7 - OpenGIS Catalogue Service Implementation Specification 2.0.2 (http://portal.opengeospatial.org/files/?artifact_id=20555) 

8""" 

9 

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 

23 

24gws.ext.new.owsService('csw') 

25 

26_cdir = gws.u.dirname(__file__) 

27 

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] 

54 

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) 

65 

66 

67class Profile(gws.Enum): 

68 """Metadata profile for CSW service.""" 

69 

70 ISO = 'ISO' 

71 """ISO 19115 metadata profile.""" 

72 DCMI = 'DCMI' 

73 """Dublin Core metadata profile.""" 

74 

75 

76class Config(server.service.Config): 

77 """CSW Service configuration""" 

78 

79 profile: Profile = Profile.ISO 

80 """Metadata profile.""" 

81 

82 

83class Object(server.service.Object): 

84 protocol = gws.OwsProtocol.CSW 

85 supportedVersions = ['2.0.2'] 

86 

87 mdMap: dict[str, gws.Metadata] 

88 profile: Profile 

89 

90 def configure(self): 

91 self.mdMap = {} 

92 self.profile = Profile.ISO 

93 

94 def configure_templates(self): 

95 extra = _DEFAULT_TEMPLATES_ISO 

96 return gws.config.util.configure_templates_for(self, extra=extra) 

97 

98 def configure_metadata(self): 

99 super().configure_metadata() 

100 self.metadata = gws.base.metadata.from_args(_DEFAULT_METADATA, self.metadata) 

101 

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 ] 

125 

126 def post_configure(self): 

127 self.collect_metadata() 

128 

129 ## 

130 

131 def parse_xml_request(self, xml): 

132 params = {} 

133 

134 params['REQUEST'] = xml.name 

135 return params 

136 

137 ## 

138 

139 def init_request(self, req): 

140 sr = super().init_request(req) 

141 sr.load_project() 

142 return sr 

143 

144 def handle_get_capabilities(self, sr: server.request.Object): 

145 return self.template_response(sr) 

146 

147 def handle_describe_record(self, sr: server.request.Object): 

148 return self.template_response(sr) 

149 

150 def handle_get_records(self, sr: server.request.Object): 

151 mds = self._find_metas(sr) 

152 

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 ) 

159 

160 return self.template_response( 

161 sr, 

162 '', 

163 metadataCollection=mdc, 

164 next=0, 

165 ) 

166 

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 [] 

170 

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 ) 

177 

178 return self.template_response( 

179 sr, 

180 '', 

181 metadataCollection=mdc, 

182 next=0, 

183 ) 

184 

185 ## 

186 

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 

196 

197 self.mdMap = {} 

198 

199 for obj in self.root.find_all(): 

200 self._collect_metadata_from_object(obj) 

201 

202 gws.log.info(f'CSW: configured with {len(self.mdMap)} records') 

203 

204 def _collect_metadata_from_object(self, obj: gws.Node): 

205 md: gws.Metadata = cast(gws.Metadata, gws.u.get(obj, 'metadata')) 

206 

207 if not md: 

208 return 

209 

210 if not md.get('catalogUid'): 

211 # gws.log.debug(f'CSW: skip {obj.uid}: no catalogUid') 

212 return 

213 

214 cid = gws.u.to_uid(md.get('catalogUid')) 

215 

216 if not self.root.app.authMgr.is_public_object(obj): 

217 gws.log.debug(f'CSW: skip {obj.uid}: not public') 

218 return 

219 

220 extra = {} 

221 

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)) 

226 

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 

233 

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) 

237 

238 self.mdMap[cid] = gws.base.metadata.from_args(md, extra) 

239 

240 ## 

241 

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 ) 

249 

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') 

254 

255 if not flt_el: 

256 return self.mdMap.values() 

257 

258 flt = gws.base.search.filter.from_fes_element(flt_el) 

259 m = gws.base.search.filter.Matcher() 

260 

261 return [md for md in self.mdMap.values() if m.matches(flt, md)] 

262 

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