Line data Source code
1 : /******************************************************************************
2 : *
3 : * Project: OGR
4 : * Purpose: Implements OGC API - Features (previously known as WFS3)
5 : * Author: Even Rouault, even dot rouault at spatialys dot com
6 : *
7 : ******************************************************************************
8 : * Copyright (c) 2018-2019, Even Rouault <even dot rouault at spatialys dot com>
9 : *
10 : * SPDX-License-Identifier: MIT
11 : ****************************************************************************/
12 :
13 : #include "ogrsf_frmts.h"
14 : #include "cpl_conv.h"
15 : #include "cpl_minixml.h"
16 : #include "cpl_http.h"
17 : #include "ogr_swq.h"
18 : #include "parsexsd.h"
19 :
20 : #include <algorithm>
21 : #include <cinttypes>
22 : #include <memory>
23 : #include <vector>
24 : #include <set>
25 :
26 : // g++ -Wshadow -Wextra -std=c++11 -fPIC -g -Wall
27 : // ogr/ogrsf_frmts/wfs/ogroapif*.cpp -shared -o ogr_OAPIF.so -Iport -Igcore
28 : // -Iogr -Iogr/ogrsf_frmts -Iogr/ogrsf_frmts/gml -Iogr/ogrsf_frmts/wfs -L.
29 : // -lgdal
30 :
31 : extern "C" void RegisterOGROAPIF();
32 :
33 : #define MEDIA_TYPE_OAPI_3_0 "application/vnd.oai.openapi+json;version=3.0"
34 : #define MEDIA_TYPE_OAPI_3_0_ALT "application/openapi+json;version=3.0"
35 : #define MEDIA_TYPE_JSON "application/json"
36 : #define MEDIA_TYPE_GEOJSON "application/geo+json"
37 : #define MEDIA_TYPE_TEXT_XML "text/xml"
38 : #define MEDIA_TYPE_APPLICATION_XML "application/xml"
39 : #define MEDIA_TYPE_JSON_SCHEMA "application/schema+json"
40 :
41 : constexpr const char *OGC_CRS84_WKT =
42 : "GEOGCRS[\"WGS 84 (CRS84)\",ENSEMBLE[\"World Geodetic System 1984 "
43 : "ensemble\",MEMBER[\"World Geodetic System 1984 "
44 : "(Transit)\"],MEMBER[\"World Geodetic System 1984 (G730)\"],MEMBER[\"World "
45 : "Geodetic System 1984 (G873)\"],MEMBER[\"World Geodetic System 1984 "
46 : "(G1150)\"],MEMBER[\"World Geodetic System 1984 (G1674)\"],MEMBER[\"World "
47 : "Geodetic System 1984 (G1762)\"],MEMBER[\"World Geodetic System 1984 "
48 : "(G2139)\"],ELLIPSOID[\"WGS "
49 : "84\",6378137,298.257223563,LENGTHUNIT[\"metre\",1]],ENSEMBLEACCURACY[2.0]]"
50 : ",PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433]],CS["
51 : "ellipsoidal,2],AXIS[\"geodetic longitude "
52 : "(Lon)\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433]],AXIS["
53 : "\"geodetic latitude "
54 : "(Lat)\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433]],USAGE["
55 : "SCOPE[\"Not "
56 : "known.\"],AREA[\"World.\"],BBOX[-90,-180,90,180]],ID[\"OGC\",\"CRS84\"]]";
57 :
58 : /************************************************************************/
59 : /* OGROAPIFDataset */
60 : /************************************************************************/
61 : class OGROAPIFLayer;
62 :
63 : class OGROAPIFDataset final : public GDALDataset
64 : {
65 : friend class OGROAPIFLayer;
66 :
67 : bool m_bMustCleanPersistent = false;
68 :
69 : // Server base URL. Like "https://example.com"
70 : // Relative links are relative to it
71 : CPLString m_osServerBaseURL{};
72 :
73 : // Service base URL. Like "https://example.com/ogcapi"
74 : CPLString m_osRootURL;
75 :
76 : CPLString m_osUserQueryParams;
77 : CPLString m_osUserPwd;
78 : int m_nPageSize = 1000;
79 : int m_nInitialRequestPageSize = 20;
80 : bool m_bPageSizeSetFromOpenOptions = false;
81 : std::vector<std::unique_ptr<OGRLayer>> m_apoLayers;
82 : std::string m_osAskedCRS{};
83 : OGRSpatialReference m_oAskedCRS{};
84 : bool m_bAskedCRSIsRequired = false;
85 : bool m_bServerFeaturesAxisOrderGISFriendly = false;
86 :
87 : bool m_bAPIDocLoaded = false;
88 : CPLJSONDocument m_oAPIDoc;
89 :
90 : bool m_bLandingPageDocLoaded = false;
91 : CPLJSONDocument m_oLandingPageDoc;
92 :
93 : bool m_bIgnoreSchema = false;
94 :
95 : std::string m_osDateTime{};
96 :
97 : bool Download(const CPLString &osURL, const char *pszAccept,
98 : CPLString &osResult, CPLString &osContentType,
99 : CPLStringList *paosHeaders = nullptr);
100 :
101 : bool DownloadJSon(const CPLString &osURL, CPLJSONDocument &oDoc,
102 : const char *pszAccept = MEDIA_TYPE_GEOJSON
103 : ", " MEDIA_TYPE_JSON,
104 : CPLStringList *paosHeaders = nullptr);
105 :
106 : bool LoadJSONCollection(const CPLJSONObject &oCollection,
107 : const CPLJSONArray &oGlobalCRSList);
108 : bool LoadJSONCollections(const CPLString &osResultIn,
109 : const std::string &osCollectionsURL);
110 :
111 : /**
112 : * Determines the page size by making a call to the API endpoint to get the server's
113 : * default and max limits for the collection items specified by itemsUrl
114 : */
115 : void DeterminePageSizeFromAPI(const std::string &itemsUrl);
116 :
117 : public:
118 41 : OGROAPIFDataset() = default;
119 : ~OGROAPIFDataset();
120 :
121 44 : int GetLayerCount() override
122 : {
123 44 : return static_cast<int>(m_apoLayers.size());
124 : }
125 :
126 : OGRLayer *GetLayer(int idx) override;
127 :
128 : bool Open(GDALOpenInfo *);
129 : const CPLJSONDocument &GetAPIDoc(std::string &osURLOut);
130 : const CPLJSONDocument &GetLandingPageDoc(std::string &osURLOut);
131 :
132 : CPLString ResolveURL(const CPLString &osURL,
133 : const std::string &osRequestURL) const;
134 : };
135 :
136 : /************************************************************************/
137 : /* OGROAPIFLayer */
138 : /************************************************************************/
139 :
140 : class OGROAPIFLayer final : public OGRLayer
141 : {
142 : OGROAPIFDataset *m_poDS = nullptr;
143 : OGRFeatureDefn *m_poFeatureDefn = nullptr;
144 : bool m_bIsGeographicCRS = false;
145 : bool m_bCRSHasGISFriendlyOrder = false;
146 : bool m_bHasEmittedContentCRSWarning = false;
147 : bool m_bHasEmittedJsonCRWarning = false;
148 : std::string m_osActiveCRS{};
149 : CPLString m_osURL;
150 : CPLString m_osPath;
151 : OGREnvelope m_oExtent;
152 : OGREnvelope m_oOriginalExtent;
153 : OGRSpatialReference m_oOriginalExtentCRS;
154 : bool m_bFeatureDefnEstablished = false;
155 : std::unique_ptr<GDALDataset> m_poUnderlyingDS;
156 : OGRLayer *m_poUnderlyingLayer = nullptr;
157 : GIntBig m_nFID = 1;
158 : CPLString m_osGetURL;
159 : CPLString m_osAttributeFilter;
160 : CPLString m_osGetID;
161 : std::vector<std::string> m_oSupportedCRSList{};
162 : OGRLayer::GetSupportedSRSListRetType m_apoSupportedCRSList{};
163 : bool m_bFilterMustBeClientSideEvaluated = false;
164 : bool m_bGotQueryableAttributes = false;
165 : std::set<CPLString> m_aoSetQueryableAttributes;
166 : bool m_bHasCQLText = false;
167 : // https://github.com/tschaub/ogcapi-features/blob/json-array-expression/extensions/cql/jfe/readme.md
168 : bool m_bHasJSONFilterExpression = false;
169 : GIntBig m_nTotalFeatureCount = -1;
170 : bool m_bHasIntIdMember = false;
171 : bool m_bHasStringIdMember = false;
172 : std::vector<std::unique_ptr<OGRFieldDefn>> m_apoFieldsFromSchema{};
173 : CPLString m_osDescribedByURL{};
174 : CPLString m_osDescribedByType{};
175 : bool m_bDescribedByIsXML = false;
176 : CPLString m_osQueryablesURL{};
177 : std::vector<std::string> m_aosItemAssetNames{}; // STAC specific
178 : CPLJSONDocument m_oCurDoc{};
179 : int m_iFeatureInPage = 0;
180 :
181 : void EstablishFeatureDefn();
182 : OGRFeature *GetNextRawFeature();
183 : CPLString AddFilters(const CPLString &osURL);
184 : CPLString BuildFilter(const swq_expr_node *poNode);
185 : CPLString BuildFilterCQLText(const swq_expr_node *poNode);
186 : CPLString BuildFilterJSONFilterExpr(const swq_expr_node *poNode);
187 : bool SupportsResultTypeHits();
188 : void GetQueryableAttributes();
189 : void GetSchema();
190 : void ComputeExtent();
191 :
192 : public:
193 : OGROAPIFLayer(OGROAPIFDataset *poDS, const CPLString &osName,
194 : const CPLJSONArray &oBBOX, const std::string &osBBOXCrs,
195 : std::vector<std::string> &&oCRSList,
196 : const std::string &osActiveCRS, double dfCoordinateEpoch,
197 : const CPLJSONArray &oLinks);
198 :
199 : ~OGROAPIFLayer();
200 :
201 : void SetItemAssets(const CPLJSONObject &oItemAssets);
202 :
203 11 : const char *GetName() override
204 : {
205 11 : return GetDescription();
206 : }
207 :
208 : OGRFeatureDefn *GetLayerDefn() override;
209 : void ResetReading() override;
210 : OGRFeature *GetNextFeature() override;
211 : OGRFeature *GetFeature(GIntBig) override;
212 : int TestCapability(const char *) override;
213 : GIntBig GetFeatureCount(int bForce = FALSE) override;
214 : OGRErr GetExtent(OGREnvelope *psExtent, int bForce = TRUE) override;
215 :
216 9 : OGRErr GetExtent(int iGeomField, OGREnvelope *psExtent, int bForce) override
217 : {
218 9 : return OGRLayer::GetExtent(iGeomField, psExtent, bForce);
219 : }
220 :
221 : void SetSpatialFilter(OGRGeometry *poGeom) override;
222 :
223 0 : void SetSpatialFilter(int iGeomField, OGRGeometry *poGeom) override
224 : {
225 0 : OGRLayer::SetSpatialFilter(iGeomField, poGeom);
226 0 : }
227 :
228 : OGRErr SetAttributeFilter(const char *pszQuery) override;
229 :
230 : const OGRLayer::GetSupportedSRSListRetType &
231 : GetSupportedSRSList(int iGeomField) override;
232 : OGRErr SetActiveSRS(int iGeomField,
233 : const OGRSpatialReference *poSRS) override;
234 : };
235 :
236 : /************************************************************************/
237 : /* CheckContentType() */
238 : /************************************************************************/
239 :
240 : // We may ask for "application/openapi+json;version=3.0"
241 : // and the server returns "application/openapi+json; charset=utf-8; version=3.0"
242 365 : static bool CheckContentType(const char *pszGotContentType,
243 : const char *pszExpectedContentType)
244 : {
245 730 : CPLStringList aosGotTokens(CSLTokenizeString2(pszGotContentType, "; ", 0));
246 : CPLStringList aosExpectedTokens(
247 730 : CSLTokenizeString2(pszExpectedContentType, "; ", 0));
248 622 : for (int i = 0; i < aosExpectedTokens.size(); i++)
249 : {
250 366 : bool bFound = false;
251 483 : for (int j = 0; j < aosGotTokens.size(); j++)
252 : {
253 374 : if (EQUAL(aosExpectedTokens[i], aosGotTokens[j]))
254 : {
255 257 : bFound = true;
256 257 : break;
257 : }
258 : }
259 366 : if (!bFound)
260 109 : return false;
261 : }
262 256 : return true;
263 : }
264 :
265 : /************************************************************************/
266 : /* ~OGROAPIFDataset() */
267 : /************************************************************************/
268 :
269 82 : OGROAPIFDataset::~OGROAPIFDataset()
270 : {
271 41 : if (m_bMustCleanPersistent)
272 : {
273 41 : char **papszOptions = CSLSetNameValue(nullptr, "CLOSE_PERSISTENT",
274 : CPLSPrintf("OAPIF:%p", this));
275 41 : CPLHTTPDestroyResult(CPLHTTPFetch(m_osRootURL, papszOptions));
276 41 : CSLDestroy(papszOptions);
277 : }
278 82 : }
279 :
280 : /************************************************************************/
281 : /* ResolveURL() */
282 : /************************************************************************/
283 :
284 : // Resolve relative links and re-inject authentication elements.
285 : // If source URL is https://user:pwd@server.com/bla
286 : // and link only contains https://server.com/bla, then insert
287 : // into it user:pwd
288 20 : CPLString OGROAPIFDataset::ResolveURL(const CPLString &osURL,
289 : const std::string &osRequestURL) const
290 : {
291 20 : const auto CleanURL = [](const std::string &osStr)
292 : {
293 20 : std::string osRet(osStr);
294 20 : const auto nPos = osRet.rfind('?');
295 20 : if (nPos != std::string::npos)
296 1 : osRet.resize(nPos);
297 20 : if (!osRet.empty() && osRet.back() == '/')
298 0 : osRet.pop_back();
299 20 : return osRet;
300 : };
301 :
302 20 : CPLString osRet(osURL);
303 : // Cf https://datatracker.ietf.org/doc/html/rfc3986#section-5.4
304 : // Partial implementation for usual cases...
305 : const std::string osRequestURLBase =
306 40 : CPLGetPathSafe(CleanURL(osRequestURL).c_str());
307 20 : if (!osURL.empty() && osURL[0] == '/')
308 1 : osRet = m_osServerBaseURL + osURL;
309 19 : else if (osURL.size() > 2 && osURL[0] == '.' && osURL[1] == '/')
310 1 : osRet = osRequestURLBase + osURL.substr(1);
311 19 : else if (osURL.size() > 3 && osURL[0] == '.' && osURL[1] == '.' &&
312 1 : osURL[2] == '/')
313 : {
314 1 : std::string osModifiedRequestURL(osRequestURLBase);
315 3 : while (osRet.size() > 3 && osRet[0] == '.' && osRet[1] == '.' &&
316 1 : osRet[2] == '/')
317 : {
318 1 : osModifiedRequestURL = CPLGetPathSafe(osModifiedRequestURL.c_str());
319 1 : osRet = osRet.substr(3);
320 : }
321 1 : osRet = osModifiedRequestURL + "/" + osRet;
322 : }
323 17 : else if (!STARTS_WITH(osURL.c_str(), "http://") &&
324 18 : !STARTS_WITH(osURL.c_str(), "https://") &&
325 1 : !STARTS_WITH(osURL.c_str(), "file://"))
326 : {
327 1 : osRet = osRequestURLBase + "/" + osURL;
328 : }
329 :
330 20 : const auto nArobaseInURLPos = m_osServerBaseURL.find('@');
331 40 : if (!osRet.empty() && STARTS_WITH(m_osServerBaseURL, "https://") &&
332 0 : STARTS_WITH(osRet, "https://") &&
333 40 : nArobaseInURLPos != std::string::npos &&
334 0 : osRet.find('@') == std::string::npos)
335 : {
336 : const auto nFirstSlashPos =
337 0 : m_osServerBaseURL.find('/', strlen("https://"));
338 0 : if (nFirstSlashPos == std::string::npos ||
339 : nFirstSlashPos > nArobaseInURLPos)
340 : {
341 : auto osUserPwd = m_osServerBaseURL.substr(
342 0 : strlen("https://"), nArobaseInURLPos - strlen("https://"));
343 : std::string osServer(
344 : nFirstSlashPos == std::string::npos
345 : ? m_osServerBaseURL.substr(nArobaseInURLPos + 1)
346 : : m_osServerBaseURL.substr(nArobaseInURLPos + 1,
347 : nFirstSlashPos -
348 0 : nArobaseInURLPos));
349 0 : if (STARTS_WITH(osRet, ("https://" + osServer).c_str()))
350 : {
351 0 : osRet = "https://" + osUserPwd + "@" +
352 0 : osRet.substr(strlen("https://"));
353 : }
354 : }
355 : }
356 40 : return osRet;
357 : }
358 :
359 : /************************************************************************/
360 : /* Download() */
361 : /************************************************************************/
362 :
363 194 : bool OGROAPIFDataset::Download(const CPLString &osURL, const char *pszAccept,
364 : CPLString &osResult, CPLString &osContentType,
365 : CPLStringList *paosHeaders)
366 : {
367 : #ifndef REMOVE_HACK
368 : VSIStatBufL sStatBuf;
369 194 : if (VSIStatL(osURL, &sStatBuf) == 0)
370 : {
371 0 : CPLDebug("OAPIF", "Reading %s", osURL.c_str());
372 0 : GByte *pabyRet = nullptr;
373 0 : if (VSIIngestFile(nullptr, osURL, &pabyRet, nullptr, -1))
374 : {
375 0 : osResult = reinterpret_cast<char *>(pabyRet);
376 0 : CPLFree(pabyRet);
377 : }
378 0 : return false;
379 : }
380 : #endif
381 194 : char **papszOptions = nullptr;
382 :
383 194 : if (pszAccept)
384 : {
385 : papszOptions =
386 193 : CSLSetNameValue(papszOptions, "HEADERS",
387 386 : (CPLString("Accept: ") + pszAccept).c_str());
388 : }
389 :
390 194 : if (!m_osUserPwd.empty())
391 : {
392 : papszOptions =
393 0 : CSLSetNameValue(papszOptions, "USERPWD", m_osUserPwd.c_str());
394 : }
395 194 : m_bMustCleanPersistent = true;
396 : papszOptions =
397 194 : CSLAddString(papszOptions, CPLSPrintf("PERSISTENT=OAPIF:%p", this));
398 388 : CPLString osURLWithQueryParameters(osURL);
399 15 : if (!m_osUserQueryParams.empty() &&
400 403 : osURL.find('?' + m_osUserQueryParams) == std::string::npos &&
401 205 : osURL.find('&' + m_osUserQueryParams) == std::string::npos)
402 : {
403 11 : if (osURL.find('?') == std::string::npos)
404 : {
405 6 : osURLWithQueryParameters += '?';
406 : }
407 : else
408 : {
409 5 : osURLWithQueryParameters += '&';
410 : }
411 11 : osURLWithQueryParameters += m_osUserQueryParams;
412 : }
413 : CPLHTTPResult *psResult =
414 194 : CPLHTTPFetch(osURLWithQueryParameters, papszOptions);
415 194 : CSLDestroy(papszOptions);
416 194 : if (!psResult)
417 0 : return false;
418 :
419 194 : if (psResult->pszErrBuf != nullptr)
420 : {
421 59 : std::string osErrorMsg(psResult->pszErrBuf);
422 59 : const char *pszData =
423 : reinterpret_cast<const char *>(psResult->pabyData);
424 59 : if (pszData)
425 : {
426 58 : osErrorMsg += ", ";
427 58 : osErrorMsg.append(pszData, CPLStrnlen(pszData, 1000));
428 : }
429 59 : CPLError(CE_Failure, CPLE_AppDefined, "%s", osErrorMsg.c_str());
430 59 : CPLHTTPDestroyResult(psResult);
431 59 : return false;
432 : }
433 :
434 135 : if (psResult->pszContentType)
435 131 : osContentType = psResult->pszContentType;
436 :
437 : // Do not check content type if not specified
438 135 : bool bFoundExpectedContentType = pszAccept ? false : true;
439 :
440 135 : if (!bFoundExpectedContentType)
441 : {
442 : #ifndef REMOVE_HACK
443 : // cppcheck-suppress nullPointer
444 134 : if (strstr(pszAccept, "json"))
445 : {
446 134 : if (strstr(osURL, "raw.githubusercontent.com") &&
447 0 : strstr(osURL, ".json"))
448 : {
449 0 : bFoundExpectedContentType = true;
450 : }
451 264 : else if (psResult->pszContentType != nullptr &&
452 130 : (CheckContentType(psResult->pszContentType,
453 53 : MEDIA_TYPE_JSON) ||
454 53 : CheckContentType(psResult->pszContentType,
455 : MEDIA_TYPE_GEOJSON)))
456 : {
457 127 : bFoundExpectedContentType = true;
458 : }
459 : }
460 : #endif
461 :
462 : // cppcheck-suppress nullPointer
463 134 : if (strstr(pszAccept, "xml") && psResult->pszContentType != nullptr &&
464 0 : (CheckContentType(psResult->pszContentType, MEDIA_TYPE_TEXT_XML) ||
465 0 : CheckContentType(psResult->pszContentType,
466 : MEDIA_TYPE_APPLICATION_XML)))
467 : {
468 0 : bFoundExpectedContentType = true;
469 : }
470 :
471 : // cppcheck-suppress nullPointer
472 269 : if (strstr(pszAccept, MEDIA_TYPE_JSON_SCHEMA) &&
473 135 : psResult->pszContentType != nullptr &&
474 1 : (CheckContentType(psResult->pszContentType, MEDIA_TYPE_JSON) ||
475 1 : CheckContentType(psResult->pszContentType,
476 : MEDIA_TYPE_JSON_SCHEMA)))
477 : {
478 1 : bFoundExpectedContentType = true;
479 : }
480 :
481 76 : for (const char *pszMediaType : {
482 : MEDIA_TYPE_JSON,
483 : MEDIA_TYPE_GEOJSON,
484 : MEDIA_TYPE_OAPI_3_0,
485 : #ifndef REMOVE_SUPPORT_FOR_OLD_VERSIONS
486 : MEDIA_TYPE_OAPI_3_0_ALT,
487 : #endif
488 210 : })
489 : {
490 : // cppcheck-suppress nullPointer
491 596 : if (strstr(pszAccept, pszMediaType) &&
492 384 : psResult->pszContentType != nullptr &&
493 180 : CheckContentType(psResult->pszContentType, pszMediaType))
494 : {
495 128 : bFoundExpectedContentType = true;
496 128 : break;
497 : }
498 : }
499 : }
500 :
501 135 : if (!bFoundExpectedContentType)
502 : {
503 5 : CPLError(CE_Failure, CPLE_AppDefined, "Unexpected Content-Type: %s",
504 5 : psResult->pszContentType ? psResult->pszContentType
505 : : "(null)");
506 5 : CPLHTTPDestroyResult(psResult);
507 5 : return false;
508 : }
509 :
510 130 : if (psResult->pabyData == nullptr)
511 : {
512 0 : CPLError(CE_Failure, CPLE_AppDefined,
513 : "Empty content returned by server");
514 0 : CPLHTTPDestroyResult(psResult);
515 0 : return false;
516 : }
517 :
518 130 : if (paosHeaders)
519 : {
520 38 : *paosHeaders = CSLDuplicate(psResult->papszHeaders);
521 : }
522 :
523 130 : osResult = reinterpret_cast<const char *>(psResult->pabyData);
524 130 : CPLHTTPDestroyResult(psResult);
525 130 : return true;
526 : }
527 :
528 : /************************************************************************/
529 : /* DownloadJSon() */
530 : /************************************************************************/
531 :
532 150 : bool OGROAPIFDataset::DownloadJSon(const CPLString &osURL,
533 : CPLJSONDocument &oDoc, const char *pszAccept,
534 : CPLStringList *paosHeaders)
535 : {
536 300 : CPLString osResult;
537 300 : CPLString osContentType;
538 150 : if (!Download(osURL, pszAccept, osResult, osContentType, paosHeaders))
539 60 : return false;
540 90 : return oDoc.LoadMemory(osResult);
541 : }
542 :
543 : /************************************************************************/
544 : /* GetLandingPageDoc() */
545 : /************************************************************************/
546 :
547 30 : const CPLJSONDocument &OGROAPIFDataset::GetLandingPageDoc(std::string &osURLOut)
548 : {
549 30 : if (m_bLandingPageDocLoaded)
550 0 : return m_oLandingPageDoc;
551 30 : m_bLandingPageDocLoaded = true;
552 30 : osURLOut = m_osRootURL;
553 30 : CPL_IGNORE_RET_VAL(
554 30 : DownloadJSon(osURLOut, m_oLandingPageDoc, MEDIA_TYPE_JSON));
555 30 : return m_oLandingPageDoc;
556 : }
557 :
558 : /************************************************************************/
559 : /* GetAPIDoc() */
560 : /************************************************************************/
561 :
562 35 : const CPLJSONDocument &OGROAPIFDataset::GetAPIDoc(std::string &osURLOut)
563 : {
564 35 : if (m_bAPIDocLoaded)
565 5 : return m_oAPIDoc;
566 30 : m_bAPIDocLoaded = true;
567 :
568 : // Fetch the /api URL from the links of the landing page
569 60 : CPLString osAPIURL;
570 60 : std::string osLandingPageURL;
571 30 : const auto &oLandingPage = GetLandingPageDoc(osLandingPageURL);
572 30 : if (oLandingPage.GetRoot().IsValid())
573 : {
574 90 : const auto oLinks = oLandingPage.GetRoot().GetArray("links");
575 30 : if (oLinks.IsValid())
576 : {
577 10 : int nCountRelAPI = 0;
578 16 : for (int i = 0; i < oLinks.Size(); i++)
579 : {
580 16 : CPLJSONObject oLink = oLinks[i];
581 32 : if (!oLink.IsValid() ||
582 16 : oLink.GetType() != CPLJSONObject::Type::Object)
583 : {
584 0 : continue;
585 : }
586 32 : const auto osRel(oLink.GetString("rel"));
587 32 : const auto osType(oLink.GetString("type"));
588 16 : if (EQUAL(osRel.c_str(), "service-desc")
589 : #ifndef REMOVE_SUPPORT_FOR_OLD_VERSIONS
590 : // Needed for http://beta.fmi.fi/data/3/wfs/sofp
591 16 : || EQUAL(osRel.c_str(), "service")
592 : #endif
593 : )
594 : {
595 10 : nCountRelAPI++;
596 : osAPIURL =
597 10 : ResolveURL(oLink.GetString("href"), osLandingPageURL);
598 10 : if (osType == MEDIA_TYPE_OAPI_3_0
599 : #ifndef REMOVE_SUPPORT_FOR_OLD_VERSIONS
600 : // Needed for http://beta.fmi.fi/data/3/wfs/sofp
601 10 : || osType == MEDIA_TYPE_OAPI_3_0_ALT
602 : #endif
603 : )
604 : {
605 10 : nCountRelAPI = 1;
606 10 : break;
607 : }
608 : }
609 : }
610 10 : if (!osAPIURL.empty() && nCountRelAPI > 1)
611 : {
612 0 : osAPIURL.clear();
613 : }
614 : }
615 : }
616 :
617 30 : const char *pszAccept = MEDIA_TYPE_OAPI_3_0
618 : #ifndef REMOVE_SUPPORT_FOR_OLD_VERSIONS
619 : ", " MEDIA_TYPE_OAPI_3_0_ALT ", " MEDIA_TYPE_JSON
620 : #endif
621 : ;
622 :
623 30 : if (!osAPIURL.empty())
624 : {
625 10 : osURLOut = osAPIURL;
626 10 : CPL_IGNORE_RET_VAL(DownloadJSon(osAPIURL, m_oAPIDoc, pszAccept));
627 10 : return m_oAPIDoc;
628 : }
629 :
630 : #ifndef REMOVE_HACK
631 20 : CPLPushErrorHandler(CPLQuietErrorHandler);
632 40 : CPLString osURL(m_osRootURL + "/api");
633 20 : osURL = CPLGetConfigOption("OGR_WFS3_API_URL", osURL.c_str());
634 20 : bool bOK = DownloadJSon(osURL, m_oAPIDoc, pszAccept);
635 20 : CPLPopErrorHandler();
636 20 : CPLErrorReset();
637 20 : if (bOK)
638 : {
639 0 : return m_oAPIDoc;
640 : }
641 :
642 20 : osURLOut = m_osRootURL + "/api/";
643 20 : if (DownloadJSon(osURLOut, m_oAPIDoc, pszAccept))
644 : {
645 0 : return m_oAPIDoc;
646 : }
647 : #endif
648 20 : return m_oAPIDoc;
649 : }
650 :
651 : /************************************************************************/
652 : /* LoadJSONCollection() */
653 : /************************************************************************/
654 :
655 36 : bool OGROAPIFDataset::LoadJSONCollection(const CPLJSONObject &oCollection,
656 : const CPLJSONArray &oGlobalCRSList)
657 : {
658 36 : if (oCollection.GetType() != CPLJSONObject::Type::Object)
659 1 : return false;
660 :
661 : // As used by https://maps.ecere.com/ogcapi/collections?f=json
662 105 : const auto osLayerDataType = oCollection.GetString("layerDataType");
663 35 : if (osLayerDataType == "Raster" || osLayerDataType == "Coverage")
664 0 : return false;
665 :
666 105 : CPLString osName(oCollection.GetString("id"));
667 : #ifndef REMOVE_SUPPORT_FOR_OLD_VERSIONS
668 35 : if (osName.empty())
669 21 : osName = oCollection.GetString("name");
670 35 : if (osName.empty())
671 1 : osName = oCollection.GetString("collectionId");
672 : #endif
673 35 : if (osName.empty())
674 1 : return false;
675 :
676 102 : CPLString osTitle(oCollection.GetString("title"));
677 102 : CPLString osDescription(oCollection.GetString("description"));
678 102 : CPLJSONArray oBBOX = oCollection.GetArray("extent/spatial/bbox");
679 : #ifndef REMOVE_HACK_FOR_NLS_FINLAND_SERVICES
680 34 : if (!oBBOX.IsValid())
681 31 : oBBOX = oCollection.GetArray("extent/spatialExtent/bbox");
682 : #endif
683 : #ifndef REMOVE_SUPPORT_FOR_OLD_VERSIONS
684 34 : if (!oBBOX.IsValid())
685 31 : oBBOX = oCollection.GetArray("extent/spatial");
686 : #endif
687 102 : const std::string osBBOXCrs = oCollection.GetString("extent/spatial/crs");
688 :
689 : // Deal with CRS list
690 102 : const CPLJSONArray oCRSListOri = oCollection.GetArray("crs");
691 68 : std::vector<std::string> oCRSList;
692 68 : std::string osActiveCRS;
693 34 : double dfCoordinateEpoch = 0.0;
694 34 : if (oCRSListOri.IsValid())
695 : {
696 12 : std::set<std::string> oSetCRS;
697 40 : for (const auto &oCRS : oCRSListOri)
698 : {
699 28 : if (oCRS.ToString() == "#/crs")
700 : {
701 4 : if (!oGlobalCRSList.IsValid())
702 : {
703 0 : CPLError(CE_Failure, CPLE_AppDefined,
704 : "Collection %s refer to #/crs global CRS list, "
705 : "which is missing",
706 : osTitle.c_str());
707 : }
708 : else
709 : {
710 8 : for (const auto &oGlobalCRS : oGlobalCRSList)
711 : {
712 12 : const auto osCRS = oGlobalCRS.ToString();
713 4 : if (oSetCRS.find(osCRS) == oSetCRS.end())
714 : {
715 4 : oSetCRS.insert(osCRS);
716 4 : oCRSList.push_back(osCRS);
717 : }
718 : }
719 : }
720 : }
721 : else
722 : {
723 72 : const auto osCRS = oCRS.ToString();
724 24 : if (oSetCRS.find(osCRS) == oSetCRS.end())
725 : {
726 24 : oSetCRS.insert(osCRS);
727 24 : oCRSList.push_back(osCRS);
728 : }
729 : }
730 : }
731 :
732 12 : if (!m_oAskedCRS.IsEmpty())
733 : {
734 8 : for (const auto &osCRS : oCRSList)
735 : {
736 6 : OGRSpatialReference oSRS;
737 6 : if (oSRS.SetFromUserInput(
738 : osCRS.c_str(),
739 : OGRSpatialReference::
740 6 : SET_FROM_USER_INPUT_LIMITATIONS_get()) ==
741 : OGRERR_NONE)
742 : {
743 6 : if (oSRS.IsSame(&m_oAskedCRS))
744 : {
745 2 : osActiveCRS = osCRS;
746 2 : break;
747 : }
748 : }
749 : }
750 4 : if (osActiveCRS.empty())
751 : {
752 2 : if (m_bAskedCRSIsRequired)
753 : {
754 1 : std::string osList;
755 3 : for (const auto &osCRS : oCRSList)
756 : {
757 2 : if (!osList.empty())
758 1 : osList += ", ";
759 2 : if (osCRS.find(
760 2 : "http://www.opengis.net/def/crs/EPSG/0/") == 0)
761 : osList +=
762 2 : "EPSG:" +
763 2 : osCRS.substr(strlen(
764 1 : "http://www.opengis.net/def/crs/EPSG/0/"));
765 1 : else if (osCRS.find("https://www.opengis.net/def/crs/"
766 1 : "EPSG/0/") == 0)
767 : osList +=
768 0 : "EPSG:" +
769 0 : osCRS.substr(strlen(
770 0 : "https://www.opengis.net/def/crs/EPSG/0/"));
771 1 : else if (osCRS.find("http://www.opengis.net/def/crs/"
772 1 : "OGC/1.3/") == 0)
773 : osList +=
774 2 : "OGC:" +
775 2 : osCRS.substr(strlen(
776 1 : "http://www.opengis.net/def/crs/OGC/1.3/"));
777 0 : else if (osCRS.find("https://www.opengis.net/def/crs/"
778 0 : "OGC/1.3/") == 0)
779 0 : osList += "OGC:" + osCRS.substr(strlen(
780 : "https://www.opengis.net/"
781 0 : "def/crs/OGC/1.3/"));
782 : else
783 0 : osList += osCRS;
784 : }
785 1 : CPLError(CE_Failure, CPLE_AppDefined,
786 : "CRS %s not found in list of CRS valid for "
787 : "collection %s. Available CRS are %s.",
788 : m_osAskedCRS.c_str(), osTitle.c_str(),
789 : osList.c_str());
790 1 : return false;
791 : }
792 : else
793 : {
794 1 : CPLDebug("OAPIF",
795 : "CRS %s not found in list of CRS valid for "
796 : "collection %s",
797 : m_osAskedCRS.c_str(), osTitle.c_str());
798 : }
799 : }
800 : }
801 : }
802 :
803 : // storageCRS is in the "OGC API - Features - Part 2: Coordinate Reference
804 : // Systems" extension
805 99 : const std::string osStorageCRS = oCollection.GetString("storageCrs");
806 : const double dfStorageCrsCoordinateEpoch =
807 33 : oCollection.GetDouble("storageCrsCoordinateEpoch");
808 33 : if (osActiveCRS.empty() || osActiveCRS == osStorageCRS)
809 : {
810 31 : osActiveCRS = osStorageCRS;
811 31 : dfCoordinateEpoch = dfStorageCrsCoordinateEpoch;
812 : }
813 :
814 99 : const auto oLinks = oCollection.GetArray("links");
815 : auto poLayer = std::make_unique<OGROAPIFLayer>(
816 33 : this, osName, oBBOX, osBBOXCrs, std::move(oCRSList), osActiveCRS,
817 66 : dfCoordinateEpoch, oLinks);
818 33 : if (!osTitle.empty())
819 2 : poLayer->SetMetadataItem("TITLE", osTitle.c_str());
820 33 : if (!osDescription.empty())
821 0 : poLayer->SetMetadataItem("DESCRIPTION", osDescription.c_str());
822 99 : auto oTemporalInterval = oCollection.GetArray("extent/temporal/interval");
823 33 : if (oTemporalInterval.IsValid() && oTemporalInterval.Size() == 1 &&
824 33 : oTemporalInterval[0].GetType() == CPLJSONObject::Type::Array)
825 : {
826 0 : auto oArray = oTemporalInterval[0].ToArray();
827 0 : if (oArray.Size() == 2)
828 : {
829 0 : if (oArray[0].GetType() == CPLJSONObject::Type::String)
830 : {
831 0 : poLayer->SetMetadataItem("TEMPORAL_INTERVAL_MIN",
832 0 : oArray[0].ToString().c_str());
833 : }
834 0 : if (oArray[1].GetType() == CPLJSONObject::Type::String)
835 : {
836 0 : poLayer->SetMetadataItem("TEMPORAL_INTERVAL_MAX",
837 0 : oArray[1].ToString().c_str());
838 : }
839 : }
840 : }
841 :
842 : // STAC specific
843 99 : auto oItemAssets = oCollection.GetObj("item_assets");
844 34 : if (oItemAssets.IsValid() &&
845 1 : oItemAssets.GetType() == CPLJSONObject::Type::Object)
846 : {
847 1 : poLayer->SetItemAssets(oItemAssets);
848 : }
849 :
850 33 : auto oJSONStr = oCollection.Format(CPLJSONObject::PrettyFormat::Pretty);
851 33 : char *apszMetadata[2] = {&oJSONStr[0], nullptr};
852 33 : poLayer->SetMetadata(apszMetadata, "json:metadata");
853 :
854 33 : m_apoLayers.emplace_back(std::move(poLayer));
855 33 : return true;
856 : }
857 :
858 : /************************************************************************/
859 : /* LoadJSONCollections() */
860 : /************************************************************************/
861 :
862 33 : bool OGROAPIFDataset::LoadJSONCollections(const CPLString &osResultIn,
863 : const std::string &osCollectionsURL)
864 : {
865 66 : std::string osParentURL(osCollectionsURL);
866 66 : CPLString osResult(osResultIn);
867 64 : while (!osResult.empty())
868 : {
869 34 : CPLJSONDocument oDoc;
870 34 : if (!oDoc.LoadMemory(osResult))
871 : {
872 1 : return false;
873 : }
874 33 : const auto &oRoot = oDoc.GetRoot();
875 66 : CPLJSONArray oCollections = oRoot.GetArray("collections");
876 33 : if (!oCollections.IsValid())
877 : {
878 2 : CPLError(CE_Failure, CPLE_AppDefined, "No collections array");
879 2 : return false;
880 : }
881 :
882 62 : const auto oGlobalCRSList = oRoot.GetArray("crs");
883 :
884 63 : for (int i = 0; i < oCollections.Size(); i++)
885 : {
886 32 : LoadJSONCollection(oCollections[i], oGlobalCRSList);
887 : }
888 :
889 31 : osResult.clear();
890 :
891 : // Paging is a (unspecified) extension to the core used by
892 : // https://{api_key}:@api.planet.com/analytics
893 62 : const auto oLinks = oRoot.GetArray("links");
894 31 : if (oLinks.IsValid())
895 : {
896 1 : CPLString osNextURL;
897 1 : int nCountRelNext = 0;
898 1 : for (int i = 0; i < oLinks.Size(); i++)
899 : {
900 1 : CPLJSONObject oLink = oLinks[i];
901 2 : if (!oLink.IsValid() ||
902 1 : oLink.GetType() != CPLJSONObject::Type::Object)
903 : {
904 0 : continue;
905 : }
906 1 : if (EQUAL(oLink.GetString("rel").c_str(), "next"))
907 : {
908 1 : osNextURL = oLink.GetString("href");
909 1 : nCountRelNext++;
910 2 : auto type = oLink.GetString("type");
911 1 : if (type == MEDIA_TYPE_GEOJSON || type == MEDIA_TYPE_JSON)
912 : {
913 1 : nCountRelNext = 1;
914 1 : break;
915 : }
916 : }
917 : }
918 1 : if (nCountRelNext == 1 && !osNextURL.empty())
919 : {
920 1 : CPLString osContentType;
921 1 : osNextURL = ResolveURL(osNextURL, osParentURL);
922 1 : osParentURL = osNextURL;
923 1 : if (!Download(osNextURL, MEDIA_TYPE_JSON, osResult,
924 : osContentType))
925 : {
926 0 : return false;
927 : }
928 : }
929 : }
930 : }
931 30 : return !m_apoLayers.empty();
932 : }
933 :
934 30 : void OGROAPIFDataset::DeterminePageSizeFromAPI(const std::string &itemsUrl)
935 : {
936 : // Try to get max limit from api
937 30 : int nMaximum{-1};
938 30 : int nDefault{-1};
939 : // Not sure if min should be considered
940 : //int nMinimum { -1 };
941 30 : std::string osAPIURL;
942 30 : const CPLJSONDocument &oDoc{GetAPIDoc(osAPIURL)};
943 30 : const auto &oRoot = oDoc.GetRoot();
944 :
945 30 : bool bFound{false};
946 :
947 : // limit from api document
948 30 : if (oRoot.IsValid())
949 : {
950 :
951 60 : const auto paths{oRoot.GetObj("paths")};
952 :
953 30 : if (paths.IsValid())
954 : {
955 :
956 10 : const auto pathName{itemsUrl.substr(m_osRootURL.length())};
957 10 : const auto path{paths.GetObj(pathName)};
958 :
959 10 : if (path.IsValid())
960 : {
961 :
962 16 : const auto parameters{path.GetArray("get/parameters")};
963 :
964 : // check $ref
965 15 : for (const auto ¶m : parameters)
966 : {
967 14 : const auto ref{param.GetString("$ref")};
968 7 : if (ref.find("limit") != std::string::npos)
969 : {
970 : // Examine ref
971 5 : if (ref.find("http") == 0 &&
972 5 : ref.find(".yml") == std::string::npos &&
973 1 : ref.find(".yaml") ==
974 : std::string::
975 : npos) // Remote document, skip yaml
976 : {
977 : // Only reinject auth if the URL matches
978 1 : auto limitUrl{ref.find(m_osRootURL) == 0
979 3 : ? ResolveURL(ref, osAPIURL)
980 2 : : ref};
981 1 : std::string fragment;
982 1 : const auto hashPos{limitUrl.find('#')};
983 1 : if (hashPos != std::string::npos)
984 : {
985 : // Remove leading #
986 1 : fragment = limitUrl.substr(hashPos + 1);
987 1 : limitUrl = limitUrl.substr(0, hashPos);
988 : }
989 1 : CPLString osResult;
990 1 : CPLString osContentType;
991 : // Do not limit accepted content-types, external resources may have any
992 1 : if (!Download(limitUrl, nullptr, osResult,
993 : osContentType))
994 : {
995 0 : CPLDebug("OAPIF",
996 : "Could not download OPENAPI $ref: %s",
997 : ref.c_str());
998 0 : return;
999 : }
1000 :
1001 : // We cannot trust the content-type, try JSON (YAML not implemented)
1002 :
1003 : // Try JSON
1004 2 : CPLJSONDocument oLimitDoc;
1005 1 : if (oLimitDoc.LoadMemory(osResult))
1006 : {
1007 2 : const auto oLimitRoot{oLimitDoc.GetRoot()};
1008 1 : if (oLimitRoot.IsValid())
1009 : {
1010 : const auto oLimit{
1011 2 : oLimitRoot.GetObj(fragment)};
1012 1 : if (oLimit.IsValid())
1013 : {
1014 1 : nMaximum = oLimit.GetInteger(
1015 : "schema/maximum", -1);
1016 : //nMinimum = oLimit.GetInteger( "schema/minimum", -1 );
1017 1 : nDefault = oLimit.GetInteger(
1018 : "schema/default", -1);
1019 1 : bFound = true;
1020 : }
1021 : }
1022 : }
1023 : }
1024 3 : else if (ref.find('#') == 0) // Local ref
1025 : {
1026 6 : const auto oLimit{oRoot.GetObj(ref.substr(1))};
1027 3 : if (oLimit.IsValid())
1028 : {
1029 3 : nMaximum =
1030 3 : oLimit.GetInteger("schema/maximum", -1);
1031 : //nMinimum = oLimit.GetInteger( "schema/minimum", -1 );
1032 3 : nDefault =
1033 3 : oLimit.GetInteger("schema/default", -1);
1034 3 : bFound = true;
1035 : }
1036 : }
1037 : else
1038 : {
1039 0 : CPLDebug("OAPIF", "Could not open OPENAPI $ref: %s",
1040 : ref.c_str());
1041 : }
1042 : }
1043 : }
1044 : }
1045 : }
1046 : }
1047 :
1048 30 : if (bFound)
1049 : {
1050 : // Initially set to GDAL's default (1000)
1051 4 : int pageSize{m_nPageSize};
1052 4 : if (nDefault > 0 && nMaximum > 0)
1053 : {
1054 : // Use the default, but if it is below GDAL's default (1000), aim for 1000
1055 : // but clamp to the maximum limit
1056 4 : pageSize = std::min(std::max(pageSize, nDefault), nMaximum);
1057 : }
1058 0 : else if (nDefault > 0)
1059 0 : pageSize = std::max(pageSize, nDefault);
1060 0 : else if (nMaximum > 0)
1061 0 : pageSize = nMaximum;
1062 :
1063 4 : if (m_nPageSize != pageSize)
1064 : {
1065 2 : CPLDebug("OAPIF", "Page size set from OPENAPI schema: %d",
1066 : pageSize);
1067 2 : m_nPageSize = pageSize;
1068 : }
1069 : }
1070 : }
1071 :
1072 : /************************************************************************/
1073 : /* ConcatenateURLParts() */
1074 : /************************************************************************/
1075 :
1076 70 : static std::string ConcatenateURLParts(const std::string &osPart1,
1077 : const std::string &osPart2)
1078 : {
1079 70 : if (!osPart1.empty() && osPart1.back() == '/' && !osPart2.empty() &&
1080 0 : osPart2.front() == '/')
1081 : {
1082 0 : return osPart1.substr(0, osPart1.size() - 1) + osPart2;
1083 : }
1084 70 : return osPart1 + osPart2;
1085 : }
1086 :
1087 : /************************************************************************/
1088 : /* Open() */
1089 : /************************************************************************/
1090 :
1091 41 : bool OGROAPIFDataset::Open(GDALOpenInfo *poOpenInfo)
1092 : {
1093 82 : CPLString osCollectionDescURL;
1094 :
1095 41 : m_osRootURL = CSLFetchNameValueDef(poOpenInfo->papszOpenOptions, "URL",
1096 41 : poOpenInfo->pszFilename);
1097 41 : if (STARTS_WITH_CI(m_osRootURL, "WFS3:"))
1098 1 : m_osRootURL = m_osRootURL.substr(strlen("WFS3:"));
1099 40 : else if (STARTS_WITH_CI(m_osRootURL, "OAPIF:"))
1100 36 : m_osRootURL = m_osRootURL.substr(strlen("OAPIF:"));
1101 4 : else if (STARTS_WITH_CI(m_osRootURL, "OAPIF_COLLECTION:"))
1102 : {
1103 : // Used by the OGCAPI driver
1104 2 : osCollectionDescURL = m_osRootURL.substr(strlen("OAPIF_COLLECTION:"));
1105 2 : m_osRootURL = osCollectionDescURL;
1106 : }
1107 :
1108 41 : const auto nPosQuestionMark = m_osRootURL.find('?');
1109 41 : if (nPosQuestionMark != std::string::npos)
1110 : {
1111 3 : m_osUserQueryParams = m_osRootURL.substr(nPosQuestionMark + 1);
1112 3 : m_osRootURL.resize(nPosQuestionMark);
1113 : }
1114 :
1115 41 : const auto nCollectionsPos = m_osRootURL.find("/collections/");
1116 41 : if (nCollectionsPos != std::string::npos)
1117 : {
1118 4 : if (osCollectionDescURL.empty())
1119 2 : osCollectionDescURL = m_osRootURL;
1120 4 : m_osRootURL.resize(nCollectionsPos);
1121 : }
1122 :
1123 : // m_osServerBaseURL is just the "https://example.com" part from
1124 : // "https://example.com/foo/bar"
1125 41 : m_osServerBaseURL = m_osRootURL;
1126 : {
1127 41 : const char *pszStr = m_osServerBaseURL.c_str();
1128 41 : const char *pszPtr = pszStr;
1129 41 : if (STARTS_WITH(pszPtr, "http://"))
1130 41 : pszPtr += strlen("http://");
1131 0 : else if (STARTS_WITH(pszPtr, "https://"))
1132 0 : pszPtr += strlen("https://");
1133 41 : pszPtr = strchr(pszPtr, '/');
1134 41 : if (pszPtr)
1135 41 : m_osServerBaseURL.assign(pszStr, pszPtr - pszStr);
1136 : }
1137 :
1138 41 : m_bIgnoreSchema = CPLTestBool(CSLFetchNameValueDef(
1139 41 : poOpenInfo->papszOpenOptions, "IGNORE_SCHEMA", "FALSE"));
1140 :
1141 82 : const int pageSize = atoi(
1142 41 : CSLFetchNameValueDef(poOpenInfo->papszOpenOptions, "PAGE_SIZE", "-1"));
1143 :
1144 41 : if (pageSize > 0)
1145 : {
1146 0 : m_nPageSize = pageSize;
1147 0 : m_bPageSizeSetFromOpenOptions = true;
1148 : }
1149 :
1150 : m_osDateTime =
1151 41 : CSLFetchNameValueDef(poOpenInfo->papszOpenOptions, "DATETIME", "");
1152 :
1153 82 : const int initialRequestPageSize = atoi(CSLFetchNameValueDef(
1154 41 : poOpenInfo->papszOpenOptions, "INITIAL_REQUEST_PAGE_SIZE", "-1"));
1155 :
1156 41 : if (initialRequestPageSize >= 1)
1157 : {
1158 2 : m_nInitialRequestPageSize = initialRequestPageSize;
1159 : }
1160 :
1161 : m_osUserPwd =
1162 41 : CSLFetchNameValueDef(poOpenInfo->papszOpenOptions, "USERPWD", "");
1163 : std::string osCRS =
1164 82 : CSLFetchNameValueDef(poOpenInfo->papszOpenOptions, "CRS", "");
1165 : std::string osPreferredCRS =
1166 82 : CSLFetchNameValueDef(poOpenInfo->papszOpenOptions, "PREFERRED_CRS", "");
1167 41 : if (!osCRS.empty())
1168 : {
1169 2 : if (!osPreferredCRS.empty())
1170 : {
1171 0 : CPLError(
1172 : CE_Failure, CPLE_AppDefined,
1173 : "CRS and PREFERRED_CRS open options are mutually exclusive.");
1174 0 : return false;
1175 : }
1176 2 : m_osAskedCRS = osCRS;
1177 2 : if (m_oAskedCRS.SetFromUserInput(
1178 : osCRS.c_str(),
1179 2 : OGRSpatialReference::SET_FROM_USER_INPUT_LIMITATIONS_get()) !=
1180 : OGRERR_NONE)
1181 : {
1182 0 : CPLError(CE_Failure, CPLE_AppDefined, "Invalid value for CRS");
1183 0 : return false;
1184 : }
1185 2 : m_bAskedCRSIsRequired = true;
1186 : }
1187 39 : else if (!osPreferredCRS.empty())
1188 : {
1189 2 : m_osAskedCRS = osPreferredCRS;
1190 2 : if (m_oAskedCRS.SetFromUserInput(
1191 : osPreferredCRS.c_str(),
1192 2 : OGRSpatialReference::SET_FROM_USER_INPUT_LIMITATIONS_get()) !=
1193 : OGRERR_NONE)
1194 : {
1195 0 : CPLError(CE_Failure, CPLE_AppDefined,
1196 : "Invalid value for PREFERRED_CRS");
1197 0 : return false;
1198 : }
1199 : }
1200 :
1201 41 : m_bServerFeaturesAxisOrderGISFriendly =
1202 41 : EQUAL(CSLFetchNameValueDef(poOpenInfo->papszOpenOptions,
1203 : "SERVER_FEATURE_AXIS_ORDER",
1204 : "AUTHORITY_COMPLIANT"),
1205 : "GIS_FRIENDLY");
1206 :
1207 82 : CPLString osResult;
1208 82 : CPLString osContentType;
1209 :
1210 41 : if (!osCollectionDescURL.empty())
1211 : {
1212 4 : if (!Download(osCollectionDescURL, MEDIA_TYPE_JSON, osResult,
1213 : osContentType))
1214 : {
1215 0 : return false;
1216 : }
1217 8 : CPLJSONDocument oDoc;
1218 4 : if (!oDoc.LoadMemory(osResult))
1219 : {
1220 0 : return false;
1221 : }
1222 4 : const auto &oRoot = oDoc.GetRoot();
1223 4 : return LoadJSONCollection(oRoot, CPLJSONArray());
1224 : }
1225 :
1226 : const std::string osCollectionsURL(
1227 111 : ConcatenateURLParts(m_osRootURL, "/collections"));
1228 37 : if (!Download(osCollectionsURL, MEDIA_TYPE_JSON, osResult, osContentType))
1229 : {
1230 4 : return false;
1231 : }
1232 :
1233 33 : if (osContentType.find("json") != std::string::npos)
1234 : {
1235 33 : return LoadJSONCollections(osResult, osCollectionsURL);
1236 : }
1237 :
1238 0 : return true;
1239 : }
1240 :
1241 : /************************************************************************/
1242 : /* GetLayer() */
1243 : /************************************************************************/
1244 :
1245 36 : OGRLayer *OGROAPIFDataset::GetLayer(int nIndex)
1246 : {
1247 36 : if (nIndex < 0 || nIndex >= GetLayerCount())
1248 0 : return nullptr;
1249 36 : return m_apoLayers[nIndex].get();
1250 : }
1251 :
1252 : /************************************************************************/
1253 : /* Identify() */
1254 : /************************************************************************/
1255 :
1256 45767 : static int OGROAPIFDriverIdentify(GDALOpenInfo *poOpenInfo)
1257 :
1258 : {
1259 91532 : return STARTS_WITH_CI(poOpenInfo->pszFilename, "WFS3:") ||
1260 45765 : STARTS_WITH_CI(poOpenInfo->pszFilename, "OAPIF:") ||
1261 137221 : STARTS_WITH_CI(poOpenInfo->pszFilename, "OAPIF_COLLECTION:") ||
1262 45689 : (poOpenInfo->IsSingleAllowedDriver("OAPIF") &&
1263 4 : (STARTS_WITH(poOpenInfo->pszFilename, "http://") ||
1264 45767 : STARTS_WITH(poOpenInfo->pszFilename, "https://")));
1265 : }
1266 :
1267 : /************************************************************************/
1268 : /* HasGISFriendlyAxisOrder() */
1269 : /************************************************************************/
1270 :
1271 20 : static bool HasGISFriendlyAxisOrder(const OGRSpatialReference *poSRS)
1272 : {
1273 20 : const auto &axisMapping = poSRS->GetDataAxisToSRSAxisMapping();
1274 38 : return axisMapping.size() >= 2 && axisMapping[0] == 1 &&
1275 38 : axisMapping[1] == 2;
1276 : }
1277 :
1278 : /************************************************************************/
1279 : /* OGROAPIFLayer() */
1280 : /************************************************************************/
1281 :
1282 33 : OGROAPIFLayer::OGROAPIFLayer(OGROAPIFDataset *poDS, const CPLString &osName,
1283 : const CPLJSONArray &oBBOX,
1284 : const std::string &osBBOXCrs,
1285 : std::vector<std::string> &&oCRSList,
1286 : const std::string &osActiveCRS,
1287 : double dfCoordinateEpoch,
1288 33 : const CPLJSONArray &oLinks)
1289 33 : : m_poDS(poDS)
1290 : {
1291 33 : m_poFeatureDefn = new OGRFeatureDefn(osName);
1292 33 : m_poFeatureDefn->Reference();
1293 33 : SetDescription(osName);
1294 33 : m_oSupportedCRSList = std::move(oCRSList);
1295 :
1296 33 : OGRSpatialReference *poSRS = new OGRSpatialReference();
1297 66 : poSRS->SetFromUserInput(
1298 33 : !osActiveCRS.empty() ? osActiveCRS.c_str() : SRS_WKT_WGS84_LAT_LONG,
1299 : OGRSpatialReference::SET_FROM_USER_INPUT_LIMITATIONS_get());
1300 33 : poSRS->SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
1301 33 : m_bIsGeographicCRS = poSRS->IsGeographic();
1302 33 : m_bCRSHasGISFriendlyOrder =
1303 33 : osActiveCRS.empty() || HasGISFriendlyAxisOrder(poSRS);
1304 33 : m_osActiveCRS = osActiveCRS;
1305 33 : if (dfCoordinateEpoch > 0)
1306 2 : poSRS->SetCoordinateEpoch(dfCoordinateEpoch);
1307 33 : m_poFeatureDefn->GetGeomFieldDefn(0)->SetSpatialRef(poSRS);
1308 :
1309 33 : poSRS->Release();
1310 :
1311 33 : if (oBBOX.IsValid() && oBBOX.Size() > 0)
1312 : {
1313 20 : CPLJSONArray oRealBBOX;
1314 : // In the final 1.0.0 spec, spatial.bbox is an array (normally with
1315 : // a single element) of 4-element arrays
1316 10 : if (oBBOX[0].GetType() == CPLJSONObject::Type::Array)
1317 : {
1318 3 : oRealBBOX = oBBOX[0].ToArray();
1319 : }
1320 : #ifndef REMOVE_SUPPORT_FOR_OLD_VERSIONS
1321 7 : else if (oBBOX.Size() == 4 || oBBOX.Size() == 6)
1322 : {
1323 7 : oRealBBOX = oBBOX;
1324 : }
1325 : #endif
1326 10 : if (oRealBBOX.Size() == 4 || oRealBBOX.Size() == 6)
1327 : {
1328 10 : m_oOriginalExtent.MinX = oRealBBOX[0].ToDouble();
1329 10 : m_oOriginalExtent.MinY = oRealBBOX[1].ToDouble();
1330 10 : m_oOriginalExtent.MaxX =
1331 10 : oRealBBOX[oRealBBOX.Size() == 6 ? 3 : 2].ToDouble();
1332 10 : m_oOriginalExtent.MaxY =
1333 10 : oRealBBOX[oRealBBOX.Size() == 6 ? 4 : 3].ToDouble();
1334 :
1335 20 : m_oOriginalExtentCRS.SetFromUserInput(
1336 10 : !osBBOXCrs.empty() ? osBBOXCrs.c_str() : OGC_CRS84_WKT,
1337 : OGRSpatialReference::SET_FROM_USER_INPUT_LIMITATIONS_get());
1338 :
1339 : // Handle bbox over antimeridian, which we do not support properly
1340 : // in OGR
1341 10 : if (m_oOriginalExtentCRS.IsGeographic())
1342 : {
1343 : const bool bSwitchXY =
1344 10 : !HasGISFriendlyAxisOrder(&m_oOriginalExtentCRS);
1345 10 : if (bSwitchXY)
1346 : {
1347 0 : std::swap(m_oOriginalExtent.MinX, m_oOriginalExtent.MinY);
1348 0 : std::swap(m_oOriginalExtent.MaxX, m_oOriginalExtent.MaxY);
1349 : }
1350 :
1351 10 : if (m_oOriginalExtent.MinX > m_oOriginalExtent.MaxX &&
1352 0 : fabs(m_oOriginalExtent.MinX) <= 180.0 &&
1353 0 : fabs(m_oOriginalExtent.MaxX) <= 180.0)
1354 : {
1355 0 : m_oOriginalExtent.MinX = -180.0;
1356 0 : m_oOriginalExtent.MaxX = 180.0;
1357 : }
1358 :
1359 10 : if (bSwitchXY)
1360 : {
1361 0 : std::swap(m_oOriginalExtent.MinX, m_oOriginalExtent.MinY);
1362 0 : std::swap(m_oOriginalExtent.MaxX, m_oOriginalExtent.MaxY);
1363 : }
1364 : }
1365 : }
1366 : }
1367 :
1368 : // Default to what the spec mandates for the /items URL, but check links
1369 : // later
1370 33 : m_osURL = ConcatenateURLParts(m_poDS->m_osRootURL,
1371 66 : "/collections/" + osName + "/items");
1372 66 : const std::string osParentURL(m_osURL);
1373 33 : m_osPath = "/collections/" + osName + "/items";
1374 :
1375 33 : if (oLinks.IsValid())
1376 : {
1377 76 : for (int i = 0; i < oLinks.Size(); i++)
1378 : {
1379 72 : CPLJSONObject oLink = oLinks[i];
1380 144 : if (!oLink.IsValid() ||
1381 72 : oLink.GetType() != CPLJSONObject::Type::Object)
1382 : {
1383 0 : continue;
1384 : }
1385 216 : const auto osRel(oLink.GetString("rel"));
1386 216 : const auto osURL = oLink.GetString("href");
1387 216 : const auto type = oLink.GetString("type");
1388 72 : if (EQUAL(osRel.c_str(), "describedby"))
1389 : {
1390 4 : if (type == MEDIA_TYPE_TEXT_XML ||
1391 2 : type == MEDIA_TYPE_APPLICATION_XML)
1392 : {
1393 1 : m_osDescribedByURL = osURL;
1394 1 : m_osDescribedByType = type;
1395 1 : m_bDescribedByIsXML = true;
1396 : }
1397 2 : else if (type == MEDIA_TYPE_JSON_SCHEMA &&
1398 1 : m_osDescribedByURL.empty())
1399 : {
1400 1 : m_osDescribedByURL = osURL;
1401 1 : m_osDescribedByType = type;
1402 1 : m_bDescribedByIsXML = false;
1403 : }
1404 : }
1405 70 : else if (EQUAL(osRel.c_str(), "queryables"))
1406 : {
1407 0 : if (type == MEDIA_TYPE_JSON || m_osQueryablesURL.empty())
1408 : {
1409 0 : m_osQueryablesURL = m_poDS->ResolveURL(osURL, osParentURL);
1410 : }
1411 : }
1412 70 : else if (EQUAL(osRel.c_str(), "items"))
1413 : {
1414 12 : if (type == MEDIA_TYPE_GEOJSON)
1415 : {
1416 2 : m_osURL = m_poDS->ResolveURL(osURL, osParentURL);
1417 : }
1418 : }
1419 : }
1420 4 : if (!m_osDescribedByURL.empty())
1421 : {
1422 : m_osDescribedByURL =
1423 2 : m_poDS->ResolveURL(m_osDescribedByURL, osParentURL);
1424 : }
1425 : }
1426 :
1427 33 : OGROAPIFLayer::ResetReading();
1428 33 : }
1429 :
1430 : /************************************************************************/
1431 : /* ~OGROAPIFLayer() */
1432 : /************************************************************************/
1433 :
1434 66 : OGROAPIFLayer::~OGROAPIFLayer()
1435 : {
1436 33 : m_poFeatureDefn->Release();
1437 66 : }
1438 :
1439 : /************************************************************************/
1440 : /* GetSupportedSRSList() */
1441 : /************************************************************************/
1442 :
1443 : const OGRLayer::GetSupportedSRSListRetType &
1444 5 : OGROAPIFLayer::GetSupportedSRSList(int /*iGeomField*/)
1445 : {
1446 5 : if (!m_oSupportedCRSList.empty() && m_apoSupportedCRSList.empty())
1447 : {
1448 6 : for (const auto &osCRS : m_oSupportedCRSList)
1449 : {
1450 : auto poSRS = std::unique_ptr<OGRSpatialReference,
1451 : OGRSpatialReferenceReleaser>(
1452 8 : new OGRSpatialReference());
1453 4 : if (poSRS->SetFromUserInput(
1454 : osCRS.c_str(),
1455 : OGRSpatialReference::
1456 4 : SET_FROM_USER_INPUT_LIMITATIONS_get()) == OGRERR_NONE)
1457 : {
1458 4 : m_apoSupportedCRSList.emplace_back(std::move(poSRS));
1459 : }
1460 : }
1461 : }
1462 5 : return m_apoSupportedCRSList;
1463 : }
1464 :
1465 : /************************************************************************/
1466 : /* SetActiveSRS() */
1467 : /************************************************************************/
1468 :
1469 5 : OGRErr OGROAPIFLayer::SetActiveSRS(int /*iGeomField*/,
1470 : const OGRSpatialReference *poSRS)
1471 : {
1472 5 : if (poSRS == nullptr)
1473 1 : return OGRERR_FAILURE;
1474 4 : const char *const apszOptions[] = {
1475 : "IGNORE_DATA_AXIS_TO_SRS_AXIS_MAPPING=YES", nullptr};
1476 7 : for (const auto &osCRS : m_oSupportedCRSList)
1477 : {
1478 6 : OGRSpatialReference oTmpSRS;
1479 6 : if (oTmpSRS.SetFromUserInput(
1480 : osCRS.c_str(),
1481 : OGRSpatialReference::SET_FROM_USER_INPUT_LIMITATIONS_get()) ==
1482 12 : OGRERR_NONE &&
1483 6 : oTmpSRS.IsSame(poSRS, apszOptions))
1484 : {
1485 3 : m_osActiveCRS = osCRS;
1486 3 : auto poGeomFieldDefn = m_poFeatureDefn->GetGeomFieldDefn(0);
1487 3 : if (poGeomFieldDefn)
1488 : {
1489 3 : OGRSpatialReference *poSRSClone = poSRS->Clone();
1490 3 : poSRSClone->SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
1491 3 : poGeomFieldDefn->SetSpatialRef(poSRSClone);
1492 3 : m_bIsGeographicCRS = poSRSClone->IsGeographic();
1493 3 : m_bCRSHasGISFriendlyOrder = HasGISFriendlyAxisOrder(poSRSClone);
1494 3 : poSRSClone->Release();
1495 : }
1496 3 : m_oExtent = OGREnvelope();
1497 3 : SetSpatialFilter(nullptr);
1498 3 : ResetReading();
1499 3 : return OGRERR_NONE;
1500 : }
1501 : }
1502 1 : return OGRERR_FAILURE;
1503 : }
1504 :
1505 : /************************************************************************/
1506 : /* ComputeExtent() */
1507 : /************************************************************************/
1508 :
1509 8 : void OGROAPIFLayer::ComputeExtent()
1510 : {
1511 8 : m_oExtent = m_oOriginalExtent;
1512 8 : const auto poGeomFieldDefn = m_poFeatureDefn->GetGeomFieldDefn(0);
1513 8 : if (poGeomFieldDefn)
1514 : {
1515 8 : const OGRSpatialReference *poSRS = poGeomFieldDefn->GetSpatialRef();
1516 8 : if (poSRS && !poSRS->IsSame(&m_oOriginalExtentCRS))
1517 : {
1518 : auto poCT = std::unique_ptr<OGRCoordinateTransformation>(
1519 8 : OGRCreateCoordinateTransformation(&m_oOriginalExtentCRS,
1520 16 : poSRS));
1521 8 : if (poCT)
1522 : {
1523 8 : poCT->TransformBounds(
1524 : m_oOriginalExtent.MinX, m_oOriginalExtent.MinY,
1525 : m_oOriginalExtent.MaxX, m_oOriginalExtent.MaxY,
1526 : &m_oExtent.MinX, &m_oExtent.MinY, &m_oExtent.MaxX,
1527 8 : &m_oExtent.MaxY, 20);
1528 : }
1529 : }
1530 : }
1531 8 : }
1532 :
1533 : /************************************************************************/
1534 : /* SetItemAssets() */
1535 : /************************************************************************/
1536 :
1537 1 : void OGROAPIFLayer::SetItemAssets(const CPLJSONObject &oItemAssets)
1538 : {
1539 2 : auto oChildren = oItemAssets.GetChildren();
1540 3 : for (const auto &oItemAsset : oChildren)
1541 : {
1542 2 : m_aosItemAssetNames.emplace_back(oItemAsset.GetName());
1543 : }
1544 1 : }
1545 :
1546 : /************************************************************************/
1547 : /* ResolveRefs() */
1548 : /************************************************************************/
1549 :
1550 205 : static CPLJSONObject ResolveRefs(const CPLJSONObject &oRoot,
1551 : const CPLJSONObject &oObj)
1552 : {
1553 615 : const auto osRef = oObj.GetString("$ref");
1554 205 : if (osRef.empty())
1555 179 : return oObj;
1556 26 : if (STARTS_WITH(osRef.c_str(), "#/"))
1557 : {
1558 25 : return oRoot.GetObj(osRef.c_str() + 2);
1559 : }
1560 2 : CPLJSONObject oInvalid;
1561 1 : oInvalid.Deinit();
1562 1 : return oInvalid;
1563 : }
1564 :
1565 : /************************************************************************/
1566 : /* BuildExampleRecursively() */
1567 : /************************************************************************/
1568 :
1569 205 : static bool BuildExampleRecursively(CPLJSONObject &oRes,
1570 : const CPLJSONObject &oRoot,
1571 : const CPLJSONObject &oObjIn)
1572 : {
1573 410 : auto oResolvedObj = ResolveRefs(oRoot, oObjIn);
1574 205 : if (!oResolvedObj.IsValid())
1575 1 : return false;
1576 612 : const auto osType = oResolvedObj.GetString("type");
1577 204 : if (osType == "object")
1578 : {
1579 87 : const auto oAllOf = oResolvedObj.GetArray("allOf");
1580 58 : const auto oProperties = oResolvedObj.GetObj("properties");
1581 29 : if (oAllOf.IsValid())
1582 : {
1583 13 : for (int i = 0; i < oAllOf.Size(); i++)
1584 : {
1585 20 : CPLJSONObject oChildRes;
1586 20 : if (BuildExampleRecursively(oChildRes, oRoot, oAllOf[i]) &&
1587 10 : oChildRes.GetType() == CPLJSONObject::Type::Object)
1588 : {
1589 20 : auto oChildren = oChildRes.GetChildren();
1590 89 : for (const auto &oChild : oChildren)
1591 : {
1592 79 : oRes.Add(oChild.GetName(), oChild);
1593 : }
1594 : }
1595 : }
1596 : }
1597 26 : else if (oProperties.IsValid())
1598 : {
1599 50 : auto oChildren = oProperties.GetChildren();
1600 206 : for (const auto &oChild : oChildren)
1601 : {
1602 362 : CPLJSONObject oChildRes;
1603 181 : if (BuildExampleRecursively(oChildRes, oRoot, oChild))
1604 : {
1605 180 : oRes.Add(oChild.GetName(), oChildRes);
1606 : }
1607 : else
1608 : {
1609 1 : oRes.Add(oChild.GetName(), "unknown type");
1610 : }
1611 : }
1612 : }
1613 29 : return true;
1614 : }
1615 175 : else if (osType == "array")
1616 : {
1617 26 : CPLJSONArray oArray;
1618 26 : const auto oItems = oResolvedObj.GetObj("items");
1619 13 : if (oItems.IsValid())
1620 : {
1621 26 : CPLJSONObject oChildRes;
1622 13 : if (BuildExampleRecursively(oChildRes, oRoot, oItems))
1623 : {
1624 12 : oArray.Add(oChildRes);
1625 : }
1626 : }
1627 13 : oRes = std::move(oArray);
1628 13 : return true;
1629 : }
1630 162 : else if (osType == "string")
1631 : {
1632 234 : CPLJSONObject oTemp;
1633 234 : const auto osFormat = oResolvedObj.GetString("format");
1634 117 : if (!osFormat.empty())
1635 27 : oTemp.Set("_", osFormat);
1636 : else
1637 90 : oTemp.Set("_", "string");
1638 117 : oRes = oTemp.GetObj("_");
1639 117 : return true;
1640 : }
1641 45 : else if (osType == "number")
1642 : {
1643 30 : CPLJSONObject oTemp;
1644 30 : oTemp.Set("_", 1.25);
1645 30 : oRes = oTemp.GetObj("_");
1646 30 : return true;
1647 : }
1648 15 : else if (osType == "integer")
1649 : {
1650 14 : CPLJSONObject oTemp;
1651 14 : oTemp.Set("_", 1);
1652 14 : oRes = oTemp.GetObj("_");
1653 14 : return true;
1654 : }
1655 1 : else if (osType == "boolean")
1656 : {
1657 0 : CPLJSONObject oTemp;
1658 0 : oTemp.Set("_", true);
1659 0 : oRes = oTemp.GetObj("_");
1660 0 : return true;
1661 : }
1662 1 : else if (osType == "null")
1663 : {
1664 0 : CPLJSONObject oTemp;
1665 0 : oTemp.SetNull("_");
1666 0 : oRes = oTemp.GetObj("_");
1667 0 : return true;
1668 : }
1669 :
1670 1 : return false;
1671 : }
1672 :
1673 : /************************************************************************/
1674 : /* GetObjectExampleFromSchema() */
1675 : /************************************************************************/
1676 :
1677 1 : static CPLJSONObject GetObjectExampleFromSchema(const std::string &osJSONSchema)
1678 : {
1679 2 : CPLJSONDocument oDoc;
1680 1 : if (!oDoc.LoadMemory(osJSONSchema))
1681 : {
1682 0 : CPLJSONObject oInvalid;
1683 0 : oInvalid.Deinit();
1684 0 : return oInvalid;
1685 : }
1686 2 : const auto &oRoot = oDoc.GetRoot();
1687 2 : CPLJSONObject oRes;
1688 1 : BuildExampleRecursively(oRes, oRoot, oRoot);
1689 1 : return oRes;
1690 : }
1691 :
1692 : /************************************************************************/
1693 : /* GetSchema() */
1694 : /************************************************************************/
1695 :
1696 30 : void OGROAPIFLayer::GetSchema()
1697 : {
1698 30 : if (m_osDescribedByURL.empty() || m_poDS->m_bIgnoreSchema)
1699 28 : return;
1700 :
1701 4 : CPLErrorStateBackuper oErrorStateBackuper(CPLQuietErrorHandler);
1702 :
1703 2 : if (m_bDescribedByIsXML)
1704 : {
1705 2 : std::vector<GMLFeatureClass *> apoClasses;
1706 1 : bool bFullyUnderstood = false;
1707 1 : bool bUseSchemaImports = false;
1708 1 : bool bHaveSchema = GMLParseXSD(m_osDescribedByURL, bUseSchemaImports,
1709 : apoClasses, bFullyUnderstood);
1710 1 : if (bHaveSchema && apoClasses.size() == 1)
1711 : {
1712 1 : CPLDebug("OAPIF", "Using XML schema");
1713 1 : auto poGMLFeatureClass = apoClasses[0];
1714 1 : if (poGMLFeatureClass->GetGeometryPropertyCount() == 1)
1715 : {
1716 : // Force linear type as we work with GeoJSON data
1717 1 : m_poFeatureDefn->SetGeomType(
1718 : OGR_GT_GetLinear(static_cast<OGRwkbGeometryType>(
1719 1 : poGMLFeatureClass->GetGeometryProperty(0)->GetType())));
1720 : }
1721 :
1722 1 : const int nPropertyCount = poGMLFeatureClass->GetPropertyCount();
1723 : // This is a hack for
1724 : // http://www.pvretano.com/cubewerx/cubeserv/default/wfs/3.0.0/framework/collections/UNINCORPORATED_PL/schema
1725 : // The GML representation has attributes starting all with
1726 : // "UNINCORPORATED_PL." whereas the GeoJSON output not
1727 2 : CPLString osPropertyNamePrefix(GetName());
1728 1 : osPropertyNamePrefix += '.';
1729 1 : bool bAllPrefixed = true;
1730 2 : for (int iField = 0; iField < nPropertyCount; iField++)
1731 : {
1732 1 : const auto poProperty = poGMLFeatureClass->GetProperty(iField);
1733 1 : if (!STARTS_WITH(poProperty->GetName(),
1734 : osPropertyNamePrefix.c_str()))
1735 : {
1736 1 : bAllPrefixed = false;
1737 : }
1738 : }
1739 2 : for (int iField = 0; iField < nPropertyCount; iField++)
1740 : {
1741 1 : const auto poProperty = poGMLFeatureClass->GetProperty(iField);
1742 1 : OGRFieldSubType eSubType = OFSTNone;
1743 : const OGRFieldType eFType =
1744 1 : GML_GetOGRFieldType(poProperty->GetType(), eSubType);
1745 :
1746 : const char *pszName =
1747 1 : poProperty->GetName() +
1748 0 : (bAllPrefixed ? osPropertyNamePrefix.size() : 0);
1749 2 : auto poField = std::make_unique<OGRFieldDefn>(pszName, eFType);
1750 1 : poField->SetSubType(eSubType);
1751 1 : m_apoFieldsFromSchema.emplace_back(std::move(poField));
1752 : }
1753 : }
1754 :
1755 2 : for (auto poFeatureClass : apoClasses)
1756 1 : delete poFeatureClass;
1757 : }
1758 : else
1759 : {
1760 2 : CPLString osContentType;
1761 2 : CPLString osResult;
1762 1 : if (!m_poDS->Download(m_osDescribedByURL, m_osDescribedByType, osResult,
1763 : osContentType))
1764 : {
1765 0 : CPLDebug("OAPIF", "Could not download schema");
1766 : }
1767 : else
1768 : {
1769 2 : const auto oExample = GetObjectExampleFromSchema(osResult);
1770 : // CPLDebug("OAPIF", "Example from schema: %s",
1771 : // oExample.Format(CPLJSONObject::PrettyFormat::Pretty).c_str());
1772 2 : if (oExample.IsValid() &&
1773 1 : oExample.GetType() == CPLJSONObject::Type::Object)
1774 : {
1775 3 : const auto oProperties = oExample.GetObj("properties");
1776 2 : if (oProperties.IsValid() &&
1777 1 : oProperties.GetType() == CPLJSONObject::Type::Object)
1778 : {
1779 1 : CPLDebug("OAPIF", "Using JSON schema");
1780 2 : const auto oProps = oProperties.GetChildren();
1781 19 : for (const auto &oProp : oProps)
1782 : {
1783 18 : OGRFieldType eType = OFTString;
1784 18 : OGRFieldSubType eSubType = OFSTNone;
1785 18 : const auto oType = oProp.GetType();
1786 18 : if (oType == CPLJSONObject::Type::String)
1787 : {
1788 13 : if (oProp.ToString() == "date-time")
1789 : {
1790 4 : eType = OFTDateTime;
1791 : }
1792 9 : else if (oProp.ToString() == "date")
1793 : {
1794 0 : eType = OFTDate;
1795 : }
1796 : }
1797 5 : else if (oType == CPLJSONObject::Type::Boolean)
1798 : {
1799 0 : eType = OFTInteger;
1800 0 : eSubType = OFSTBoolean;
1801 : }
1802 5 : else if (oType == CPLJSONObject::Type::Double)
1803 : {
1804 0 : eType = OFTReal;
1805 : }
1806 5 : else if (oType == CPLJSONObject::Type::Integer)
1807 : {
1808 0 : eType = OFTInteger;
1809 : }
1810 5 : else if (oType == CPLJSONObject::Type::Long)
1811 : {
1812 0 : eType = OFTInteger64;
1813 : }
1814 5 : else if (oType == CPLJSONObject::Type::Array)
1815 : {
1816 4 : const auto oArray = oProp.ToArray();
1817 2 : if (oArray.Size() > 0)
1818 : {
1819 1 : if (oArray[0].GetType() ==
1820 : CPLJSONObject::Type::String)
1821 0 : eType = OFTStringList;
1822 1 : else if (oArray[0].GetType() ==
1823 : CPLJSONObject::Type::Integer)
1824 0 : eType = OFTIntegerList;
1825 : }
1826 : }
1827 :
1828 : auto poField = std::make_unique<OGRFieldDefn>(
1829 36 : oProp.GetName().c_str(), eType);
1830 18 : poField->SetSubType(eSubType);
1831 18 : m_apoFieldsFromSchema.emplace_back(std::move(poField));
1832 : }
1833 : }
1834 : }
1835 : }
1836 : }
1837 : }
1838 :
1839 : /************************************************************************/
1840 : /* GetLayerDefn() */
1841 : /************************************************************************/
1842 :
1843 114 : OGRFeatureDefn *OGROAPIFLayer::GetLayerDefn()
1844 : {
1845 114 : if (!m_bFeatureDefnEstablished)
1846 25 : EstablishFeatureDefn();
1847 114 : return m_poFeatureDefn;
1848 : }
1849 :
1850 : /************************************************************************/
1851 : /* EstablishFeatureDefn() */
1852 : /************************************************************************/
1853 :
1854 30 : void OGROAPIFLayer::EstablishFeatureDefn()
1855 : {
1856 30 : CPLAssert(!m_bFeatureDefnEstablished);
1857 30 : m_bFeatureDefnEstablished = true;
1858 :
1859 30 : GetSchema();
1860 :
1861 30 : if (!m_poDS->m_bPageSizeSetFromOpenOptions)
1862 : {
1863 30 : const int nOldPageSize{m_poDS->m_nPageSize};
1864 30 : m_poDS->DeterminePageSizeFromAPI(m_osURL);
1865 : // cppcheck-suppress knownConditionTrueFalse
1866 30 : if (nOldPageSize != m_poDS->m_nPageSize)
1867 : {
1868 4 : m_osGetURL = CPLURLAddKVP(m_osGetURL, "limit",
1869 4 : CPLSPrintf("%d", m_poDS->m_nPageSize));
1870 : }
1871 : }
1872 :
1873 30 : CPLJSONDocument oDoc;
1874 30 : CPLString osURL(m_osURL);
1875 :
1876 60 : osURL = CPLURLAddKVP(
1877 : osURL, "limit",
1878 30 : CPLSPrintf("%d", std::min(m_poDS->m_nInitialRequestPageSize,
1879 60 : m_poDS->m_nPageSize)));
1880 30 : if (!m_poDS->DownloadJSon(osURL, oDoc))
1881 0 : return;
1882 :
1883 30 : const CPLString osTmpFilename(VSIMemGenerateHiddenFilename("oapif.json"));
1884 30 : oDoc.Save(osTmpFilename);
1885 : std::unique_ptr<GDALDataset> poDS(GDALDataset::FromHandle(
1886 : GDALOpenEx(osTmpFilename, GDAL_OF_VECTOR | GDAL_OF_INTERNAL, nullptr,
1887 30 : nullptr, nullptr)));
1888 30 : VSIUnlink(osTmpFilename);
1889 30 : if (!poDS.get())
1890 1 : return;
1891 29 : OGRLayer *poLayer = poDS->GetLayer(0);
1892 29 : if (!poLayer)
1893 0 : return;
1894 29 : OGRFeatureDefn *poFeatureDefn = poLayer->GetLayerDefn();
1895 29 : if (m_poFeatureDefn->GetGeomType() == wkbUnknown)
1896 : {
1897 28 : m_poFeatureDefn->SetGeomType(poFeatureDefn->GetGeomType());
1898 : }
1899 29 : if (m_apoFieldsFromSchema.empty())
1900 : {
1901 85 : for (int i = 0; i < poFeatureDefn->GetFieldCount(); i++)
1902 : {
1903 58 : m_poFeatureDefn->AddFieldDefn(poFeatureDefn->GetFieldDefn(i));
1904 : }
1905 : }
1906 : else
1907 : {
1908 3 : if (poFeatureDefn->GetFieldCount() > 0 &&
1909 1 : strcmp(poFeatureDefn->GetFieldDefn(0)->GetNameRef(), "id") == 0)
1910 : {
1911 0 : m_poFeatureDefn->AddFieldDefn(poFeatureDefn->GetFieldDefn(0));
1912 : }
1913 21 : for (const auto &poField : m_apoFieldsFromSchema)
1914 : {
1915 19 : m_poFeatureDefn->AddFieldDefn(poField.get());
1916 : }
1917 : // In case there would be properties found in sample, but not in
1918 : // schema...
1919 3 : for (int i = 0; i < poFeatureDefn->GetFieldCount(); i++)
1920 : {
1921 1 : auto poFDefn = poFeatureDefn->GetFieldDefn(i);
1922 1 : if (m_poFeatureDefn->GetFieldIndex(poFDefn->GetNameRef()) < 0)
1923 : {
1924 1 : m_poFeatureDefn->AddFieldDefn(poFDefn);
1925 : }
1926 : }
1927 : }
1928 :
1929 31 : for (const auto &osItemAsset : m_aosItemAssetNames)
1930 : {
1931 4 : OGRFieldDefn oFieldDefn(("asset_" + osItemAsset + "_href").c_str(),
1932 4 : OFTString);
1933 : // cppcheck-suppress danglingTemporaryLifetime
1934 2 : m_poFeatureDefn->AddFieldDefn(&oFieldDefn);
1935 : }
1936 :
1937 58 : const auto &oRoot = oDoc.GetRoot();
1938 29 : GIntBig nFeatures = oRoot.GetLong("numberMatched", -1);
1939 29 : if (nFeatures >= 0)
1940 : {
1941 9 : m_nTotalFeatureCount = nFeatures;
1942 : }
1943 :
1944 87 : auto oFeatures = oRoot.GetArray("features");
1945 29 : if (oFeatures.IsValid() && oFeatures.Size() > 0)
1946 : {
1947 25 : auto eType = oFeatures[0].GetObj("id").GetType();
1948 25 : if (eType == CPLJSONObject::Type::Integer ||
1949 : eType == CPLJSONObject::Type::Long)
1950 : {
1951 3 : m_bHasIntIdMember = true;
1952 : }
1953 22 : else if (eType == CPLJSONObject::Type::String)
1954 : {
1955 7 : m_bHasStringIdMember = true;
1956 : }
1957 : }
1958 : }
1959 :
1960 : /************************************************************************/
1961 : /* ResetReading() */
1962 : /************************************************************************/
1963 :
1964 79 : void OGROAPIFLayer::ResetReading()
1965 : {
1966 79 : m_poUnderlyingDS.reset();
1967 79 : m_poUnderlyingLayer = nullptr;
1968 79 : m_nFID = 1;
1969 79 : m_osGetURL = m_osURL;
1970 79 : if (!m_osGetID.empty())
1971 : {
1972 4 : m_osGetURL += "/" + m_osGetID;
1973 : }
1974 : else
1975 : {
1976 75 : if (m_poDS->m_nPageSize > 0)
1977 : {
1978 150 : m_osGetURL = CPLURLAddKVP(m_osGetURL, "limit",
1979 150 : CPLSPrintf("%d", m_poDS->m_nPageSize));
1980 : }
1981 75 : m_osGetURL = AddFilters(m_osGetURL);
1982 : }
1983 79 : m_oCurDoc = CPLJSONDocument();
1984 79 : m_iFeatureInPage = 0;
1985 79 : }
1986 :
1987 : /************************************************************************/
1988 : /* AddFilters() */
1989 : /************************************************************************/
1990 :
1991 77 : CPLString OGROAPIFLayer::AddFilters(const CPLString &osURL)
1992 : {
1993 77 : CPLString osURLNew(osURL);
1994 77 : if (m_poFilterGeom)
1995 : {
1996 8 : double dfMinX = m_sFilterEnvelope.MinX;
1997 8 : double dfMinY = m_sFilterEnvelope.MinY;
1998 8 : double dfMaxX = m_sFilterEnvelope.MaxX;
1999 8 : double dfMaxY = m_sFilterEnvelope.MaxY;
2000 8 : bool bAddBBoxFilter = true;
2001 8 : if (m_bIsGeographicCRS)
2002 : {
2003 6 : dfMinX = std::max(dfMinX, -180.0);
2004 6 : dfMinY = std::max(dfMinY, -90.0);
2005 6 : dfMaxX = std::min(dfMaxX, 180.0);
2006 6 : dfMaxY = std::min(dfMaxY, 90.0);
2007 8 : bAddBBoxFilter = dfMinX > -180.0 || dfMinY > -90.0 ||
2008 8 : dfMaxX < 180.0 || dfMaxY < 90.0;
2009 : }
2010 8 : if (bAddBBoxFilter)
2011 : {
2012 7 : if (!m_bCRSHasGISFriendlyOrder)
2013 : {
2014 2 : std::swap(dfMinX, dfMinY);
2015 2 : std::swap(dfMaxX, dfMaxY);
2016 : }
2017 14 : osURLNew = CPLURLAddKVP(osURLNew, "bbox",
2018 : CPLSPrintf("%.17g,%.17g,%.17g,%.17g",
2019 7 : dfMinX, dfMinY, dfMaxX, dfMaxY));
2020 7 : if (!m_osActiveCRS.empty())
2021 : {
2022 : osURLNew =
2023 4 : CPLURLAddKVP(osURLNew, "bbox-crs", m_osActiveCRS.c_str());
2024 : }
2025 : }
2026 : }
2027 77 : if (!m_osActiveCRS.empty())
2028 : {
2029 17 : osURLNew = CPLURLAddKVP(osURLNew, "crs", m_osActiveCRS.c_str());
2030 : }
2031 77 : if (!m_osAttributeFilter.empty())
2032 : {
2033 15 : if (osURLNew.find('?') == std::string::npos)
2034 0 : osURLNew += "?";
2035 : else
2036 15 : osURLNew += "&";
2037 15 : osURLNew += m_osAttributeFilter;
2038 : }
2039 77 : if (!m_poDS->m_osDateTime.empty())
2040 : {
2041 3 : if (osURLNew.find('?') == std::string::npos)
2042 0 : osURLNew += "?";
2043 : else
2044 3 : osURLNew += "&";
2045 3 : osURLNew += "datetime=";
2046 3 : osURLNew += m_poDS->m_osDateTime;
2047 : }
2048 77 : return osURLNew;
2049 : }
2050 :
2051 : /************************************************************************/
2052 : /* GetNextRawFeature() */
2053 : /************************************************************************/
2054 :
2055 45 : OGRFeature *OGROAPIFLayer::GetNextRawFeature()
2056 : {
2057 45 : if (!m_bFeatureDefnEstablished)
2058 4 : EstablishFeatureDefn();
2059 :
2060 45 : OGRFeature *poSrcFeature = nullptr;
2061 : while (true)
2062 : {
2063 52 : if (m_poUnderlyingLayer == nullptr)
2064 : {
2065 41 : if (m_osGetURL.empty())
2066 3 : return nullptr;
2067 :
2068 38 : m_oCurDoc = CPLJSONDocument();
2069 :
2070 38 : const CPLString osURL(m_osGetURL);
2071 38 : m_osGetURL.clear();
2072 38 : CPLStringList aosHeaders;
2073 38 : if (!m_poDS->DownloadJSon(osURL, m_oCurDoc,
2074 : MEDIA_TYPE_GEOJSON ", " MEDIA_TYPE_JSON,
2075 : &aosHeaders))
2076 : {
2077 0 : return nullptr;
2078 : }
2079 :
2080 : const std::string osContentCRS =
2081 38 : aosHeaders.FetchNameValueDef("Content-Crs", "");
2082 38 : if (!m_bHasEmittedContentCRSWarning && !osContentCRS.empty())
2083 : {
2084 7 : if (m_osActiveCRS.empty())
2085 : {
2086 0 : if (osContentCRS !=
2087 0 : "<http://www.opengis.net/def/crs/OGC/1.3/CRS84>" &&
2088 0 : osContentCRS !=
2089 : "<http://www.opengis.net/def/crs/OGC/0/CRS84h>")
2090 : {
2091 0 : m_bHasEmittedContentCRSWarning = true;
2092 0 : CPLDebug("OAPIF",
2093 : "Got Content-CRS = %s, but expected OGC:CRS84 "
2094 : "instead. "
2095 : "Content-CRS will be ignored",
2096 : osContentCRS.c_str());
2097 : }
2098 : }
2099 : else
2100 : {
2101 7 : if (osContentCRS != '<' + m_osActiveCRS + '>')
2102 : {
2103 0 : m_bHasEmittedContentCRSWarning = true;
2104 0 : CPLDebug(
2105 : "OAPIF",
2106 : "Got Content-CRS = %s, but expected %s instead. "
2107 : "Content-CRS will be ignored",
2108 : osContentCRS.c_str(), m_osActiveCRS.c_str());
2109 : }
2110 : }
2111 : }
2112 31 : else if (!m_bHasEmittedContentCRSWarning)
2113 : {
2114 31 : if (!m_osActiveCRS.empty())
2115 : {
2116 1 : m_bHasEmittedContentCRSWarning = true;
2117 1 : CPLDebug("OAPIF",
2118 : "Dit not get Content-CRS header. "
2119 : "Assuming %s is returned",
2120 : m_osActiveCRS.c_str());
2121 : }
2122 : }
2123 :
2124 38 : if (!m_bHasEmittedJsonCRWarning)
2125 : {
2126 114 : const auto oJsonCRS = m_oCurDoc.GetRoot().GetObj("crs");
2127 38 : if (oJsonCRS.IsValid())
2128 : {
2129 0 : m_bHasEmittedJsonCRWarning = true;
2130 0 : CPLDebug("OAPIF",
2131 : "JSON response contains %s. It will be ignored.",
2132 0 : oJsonCRS.ToString().c_str());
2133 : }
2134 : }
2135 :
2136 : const CPLString osTmpFilename(
2137 38 : VSIMemGenerateHiddenFilename("oapif.json"));
2138 38 : m_oCurDoc.Save(osTmpFilename);
2139 : m_poUnderlyingDS =
2140 76 : std::unique_ptr<GDALDataset>(GDALDataset::FromHandle(
2141 : GDALOpenEx(osTmpFilename, GDAL_OF_VECTOR | GDAL_OF_INTERNAL,
2142 38 : nullptr, nullptr, nullptr)));
2143 38 : VSIUnlink(osTmpFilename);
2144 38 : if (!m_poUnderlyingDS.get())
2145 : {
2146 0 : return nullptr;
2147 : }
2148 38 : m_poUnderlyingLayer = m_poUnderlyingDS->GetLayer(0);
2149 38 : if (!m_poUnderlyingLayer)
2150 : {
2151 0 : m_poUnderlyingDS.reset();
2152 0 : return nullptr;
2153 : }
2154 :
2155 : // To avoid issues with implementations having a non-relevant
2156 : // next link, make sure the current page is not empty
2157 : // We could even check that the feature count is the page size
2158 : // actually
2159 38 : if (m_poUnderlyingLayer->GetFeatureCount() > 0 && m_osGetID.empty())
2160 : {
2161 108 : CPLJSONArray oLinks = m_oCurDoc.GetRoot().GetArray("links");
2162 36 : if (oLinks.IsValid())
2163 : {
2164 6 : int nCountRelNext = 0;
2165 12 : std::string osNextURL;
2166 12 : for (int i = 0; i < oLinks.Size(); i++)
2167 : {
2168 10 : CPLJSONObject oLink = oLinks[i];
2169 20 : if (!oLink.IsValid() ||
2170 10 : oLink.GetType() != CPLJSONObject::Type::Object)
2171 : {
2172 0 : continue;
2173 : }
2174 10 : if (EQUAL(oLink.GetString("rel").c_str(), "next"))
2175 : {
2176 4 : nCountRelNext++;
2177 8 : auto type = oLink.GetString("type");
2178 4 : if (type == MEDIA_TYPE_GEOJSON ||
2179 0 : type == MEDIA_TYPE_JSON)
2180 : {
2181 4 : m_osGetURL = oLink.GetString("href");
2182 4 : break;
2183 : }
2184 0 : else if (type.empty())
2185 : {
2186 0 : osNextURL = oLink.GetString("href");
2187 : }
2188 : }
2189 : }
2190 6 : if (nCountRelNext == 1 && m_osGetURL.empty())
2191 : {
2192 : // In case we go a "rel": "next" without a "type"
2193 0 : m_osGetURL = std::move(osNextURL);
2194 : }
2195 : }
2196 :
2197 : #ifdef no_longer_used
2198 : // Recommendation /rec/core/link-header
2199 : if (m_osGetURL.empty())
2200 : {
2201 : for (int i = 0; i < aosHeaders.size(); i++)
2202 : {
2203 : CPLDebug("OAPIF", "%s", aosHeaders[i]);
2204 : if (STARTS_WITH_CI(aosHeaders[i], "Link=") &&
2205 : strstr(aosHeaders[i], "rel=\"next\"") &&
2206 : strstr(aosHeaders[i],
2207 : "type=\"" MEDIA_TYPE_GEOJSON "\""))
2208 : {
2209 : const char *pszStart = strchr(aosHeaders[i], '<');
2210 : if (pszStart)
2211 : {
2212 : const char *pszEnd = strchr(pszStart + 1, '>');
2213 : if (pszEnd)
2214 : {
2215 : m_osGetURL = pszStart + 1;
2216 : m_osGetURL.resize(pszEnd - pszStart - 1);
2217 : }
2218 : }
2219 : break;
2220 : }
2221 : }
2222 : }
2223 : #endif
2224 :
2225 36 : if (!m_osGetURL.empty())
2226 : {
2227 4 : m_osGetURL = m_poDS->ResolveURL(m_osGetURL, osURL);
2228 : }
2229 : }
2230 : }
2231 :
2232 : // cppcheck-suppress nullPointerRedundantCheck
2233 49 : poSrcFeature = m_poUnderlyingLayer->GetNextFeature();
2234 49 : if (poSrcFeature)
2235 : {
2236 42 : break;
2237 : }
2238 7 : m_poUnderlyingDS.reset();
2239 7 : m_poUnderlyingLayer = nullptr;
2240 7 : m_iFeatureInPage = 0;
2241 7 : }
2242 :
2243 42 : OGRFeature *poFeature = new OGRFeature(m_poFeatureDefn);
2244 42 : poFeature->SetFrom(poSrcFeature);
2245 :
2246 : // Collect STAC assets href
2247 2 : if (!m_aosItemAssetNames.empty() && m_poUnderlyingLayer != nullptr &&
2248 44 : m_oCurDoc.GetRoot().GetArray("features").Size() ==
2249 46 : m_poUnderlyingLayer->GetFeatureCount() &&
2250 44 : m_iFeatureInPage < m_oCurDoc.GetRoot().GetArray("features").Size())
2251 : {
2252 : auto m_oFeature =
2253 6 : m_oCurDoc.GetRoot().GetArray("features")[m_iFeatureInPage];
2254 6 : auto oAssets = m_oFeature["assets"];
2255 6 : for (const auto &osAssetName : m_aosItemAssetNames)
2256 : {
2257 12 : auto href = oAssets[osAssetName]["href"];
2258 4 : if (href.IsValid() && href.GetType() == CPLJSONObject::Type::String)
2259 : {
2260 2 : poFeature->SetField(("asset_" + osAssetName + "_href").c_str(),
2261 4 : href.ToString().c_str());
2262 : }
2263 : }
2264 : }
2265 42 : m_iFeatureInPage++;
2266 :
2267 42 : auto poGeom = poFeature->GetGeometryRef();
2268 42 : if (poGeom)
2269 : {
2270 20 : if (!m_bCRSHasGISFriendlyOrder &&
2271 3 : !m_poDS->m_bServerFeaturesAxisOrderGISFriendly)
2272 2 : poGeom->swapXY();
2273 20 : poGeom->assignSpatialReference(GetSpatialRef());
2274 : }
2275 42 : if (m_bHasIntIdMember)
2276 : {
2277 4 : poFeature->SetFID(poSrcFeature->GetFID());
2278 : }
2279 : else
2280 : {
2281 38 : poFeature->SetFID(m_nFID);
2282 38 : m_nFID++;
2283 : }
2284 42 : delete poSrcFeature;
2285 42 : return poFeature;
2286 : }
2287 :
2288 : /************************************************************************/
2289 : /* GetFeature() */
2290 : /************************************************************************/
2291 :
2292 1 : OGRFeature *OGROAPIFLayer::GetFeature(GIntBig nFID)
2293 : {
2294 1 : if (!m_bFeatureDefnEstablished)
2295 0 : EstablishFeatureDefn();
2296 1 : if (!m_bHasIntIdMember)
2297 0 : return OGRLayer::GetFeature(nFID);
2298 :
2299 1 : m_osGetID.Printf(CPL_FRMT_GIB, nFID);
2300 1 : ResetReading();
2301 1 : auto poRet = GetNextRawFeature();
2302 1 : m_osGetID.clear();
2303 1 : ResetReading();
2304 1 : return poRet;
2305 : }
2306 :
2307 : /************************************************************************/
2308 : /* GetNextFeature() */
2309 : /************************************************************************/
2310 :
2311 44 : OGRFeature *OGROAPIFLayer::GetNextFeature()
2312 : {
2313 : while (true)
2314 : {
2315 44 : OGRFeature *poFeature = GetNextRawFeature();
2316 44 : if (poFeature == nullptr)
2317 3 : return nullptr;
2318 :
2319 88 : if ((m_poFilterGeom == nullptr ||
2320 82 : FilterGeometry(poFeature->GetGeometryRef())) &&
2321 41 : (m_poAttrQuery == nullptr || !m_bFilterMustBeClientSideEvaluated ||
2322 4 : m_poAttrQuery->Evaluate(poFeature)))
2323 : {
2324 40 : return poFeature;
2325 : }
2326 : else
2327 : {
2328 1 : delete poFeature;
2329 : }
2330 1 : }
2331 : }
2332 :
2333 : /************************************************************************/
2334 : /* SupportsResultTypeHits() */
2335 : /************************************************************************/
2336 :
2337 4 : bool OGROAPIFLayer::SupportsResultTypeHits()
2338 : {
2339 8 : std::string osAPIURL;
2340 8 : CPLJSONDocument oDoc = m_poDS->GetAPIDoc(osAPIURL);
2341 4 : if (oDoc.GetRoot().GetString("openapi").empty())
2342 2 : return false;
2343 :
2344 : CPLJSONArray oParameters =
2345 4 : oDoc.GetRoot().GetObj("paths").GetObj(m_osPath).GetObj("get").GetArray(
2346 6 : "parameters");
2347 2 : if (!oParameters.IsValid())
2348 0 : return false;
2349 2 : for (int i = 0; i < oParameters.Size(); i++)
2350 : {
2351 2 : CPLJSONObject oParam = oParameters[i];
2352 4 : CPLString osRef = oParam.GetString("$ref");
2353 2 : if (!osRef.empty() && osRef.find("#/") == 0)
2354 : {
2355 2 : oParam = oDoc.GetRoot().GetObj(osRef.substr(2));
2356 : #ifndef REMOVE_HACK
2357 : // Needed for
2358 : // http://www.pvretano.com/cubewerx/cubeserv/default/wfs/3.0.0/foundation/api
2359 : // that doesn't define #/components/parameters/resultType
2360 2 : if (osRef == "#/components/parameters/resultType")
2361 2 : return true;
2362 : #endif
2363 : }
2364 0 : if (oParam.GetString("name") == "resultType" &&
2365 0 : oParam.GetString("in") == "query")
2366 : {
2367 0 : CPLJSONArray oEnum = oParam.GetArray("schema/enum");
2368 0 : for (int j = 0; j < oEnum.Size(); j++)
2369 : {
2370 0 : if (oEnum[j].ToString() == "hits")
2371 0 : return true;
2372 : }
2373 0 : return false;
2374 : }
2375 : }
2376 :
2377 0 : return false;
2378 : }
2379 :
2380 : /************************************************************************/
2381 : /* GetFeatureCount() */
2382 : /************************************************************************/
2383 :
2384 10 : GIntBig OGROAPIFLayer::GetFeatureCount(int bForce)
2385 : {
2386 :
2387 20 : if (m_poFilterGeom == nullptr && m_poAttrQuery == nullptr &&
2388 10 : m_poDS->m_osDateTime.empty())
2389 : {
2390 9 : GetLayerDefn();
2391 9 : if (m_nTotalFeatureCount >= 0)
2392 : {
2393 6 : return m_nTotalFeatureCount;
2394 : }
2395 : }
2396 :
2397 4 : if (SupportsResultTypeHits() && !m_bFilterMustBeClientSideEvaluated)
2398 : {
2399 2 : CPLString osURL(m_osURL);
2400 2 : osURL = CPLURLAddKVP(osURL, "resultType", "hits");
2401 2 : osURL = AddFilters(osURL);
2402 : #ifndef REMOVE_HACK
2403 2 : bool bGMLRequest = m_osURL.find("cubeserv") != std::string::npos;
2404 : #else
2405 : constexpr bool bGMLRequest = false;
2406 : #endif
2407 2 : if (bGMLRequest)
2408 : {
2409 0 : CPLString osResult;
2410 0 : CPLString osContentType;
2411 0 : if (m_poDS->Download(osURL, MEDIA_TYPE_TEXT_XML, osResult,
2412 : osContentType))
2413 : {
2414 0 : CPLXMLNode *psDoc = CPLParseXMLString(osResult);
2415 0 : if (psDoc)
2416 : {
2417 0 : CPLXMLTreeCloser oCloser(psDoc);
2418 0 : CPL_IGNORE_RET_VAL(oCloser);
2419 0 : CPLStripXMLNamespace(psDoc, nullptr, true);
2420 : CPLString osNumberMatched = CPLGetXMLValue(
2421 0 : psDoc, "=FeatureCollection.numberMatched", "");
2422 0 : if (!osNumberMatched.empty())
2423 0 : return CPLAtoGIntBig(osNumberMatched);
2424 : }
2425 : }
2426 : }
2427 : else
2428 : {
2429 2 : CPLJSONDocument oDoc;
2430 2 : if (m_poDS->DownloadJSon(osURL, oDoc))
2431 : {
2432 2 : GIntBig nFeatures = oDoc.GetRoot().GetLong("numberMatched", -1);
2433 2 : if (nFeatures >= 0)
2434 2 : return nFeatures;
2435 : }
2436 : }
2437 : }
2438 :
2439 2 : return OGRLayer::GetFeatureCount(bForce);
2440 : }
2441 :
2442 : /************************************************************************/
2443 : /* GetExtent() */
2444 : /************************************************************************/
2445 :
2446 10 : OGRErr OGROAPIFLayer::GetExtent(OGREnvelope *psEnvelope, int bForce)
2447 : {
2448 10 : if (m_oOriginalExtent.IsInit())
2449 : {
2450 10 : if (!m_oExtent.IsInit())
2451 8 : ComputeExtent();
2452 10 : *psEnvelope = m_oExtent;
2453 10 : return OGRERR_NONE;
2454 : }
2455 0 : return OGRLayer::GetExtent(psEnvelope, bForce);
2456 : }
2457 :
2458 : /************************************************************************/
2459 : /* SetSpatialFilter() */
2460 : /************************************************************************/
2461 :
2462 10 : void OGROAPIFLayer::SetSpatialFilter(OGRGeometry *poGeomIn)
2463 : {
2464 10 : InstallFilter(poGeomIn);
2465 :
2466 10 : ResetReading();
2467 10 : }
2468 :
2469 : /************************************************************************/
2470 : /* OGRWF3ParseDateTime() */
2471 : /************************************************************************/
2472 :
2473 5 : static int OGRWF3ParseDateTime(const char *pszValue, int &nYear, int &nMonth,
2474 : int &nDay, int &nHour, int &nMinute,
2475 : int &nSecond)
2476 : {
2477 5 : int ret = sscanf(pszValue, "%04d/%02d/%02d %02d:%02d:%02d", &nYear, &nMonth,
2478 : &nDay, &nHour, &nMinute, &nSecond);
2479 5 : if (ret >= 3)
2480 0 : return ret;
2481 5 : return sscanf(pszValue, "%04d-%02d-%02dT%02d:%02d:%02d", &nYear, &nMonth,
2482 5 : &nDay, &nHour, &nMinute, &nSecond);
2483 : }
2484 :
2485 : /************************************************************************/
2486 : /* SerializeDateTime() */
2487 : /************************************************************************/
2488 :
2489 5 : static CPLString SerializeDateTime(int nDateComponents, int nYear, int nMonth,
2490 : int nDay, int nHour, int nMinute,
2491 : int nSecond)
2492 : {
2493 5 : CPLString osRet;
2494 5 : osRet.Printf("%04d-%02d-%02dT", nYear, nMonth, nDay);
2495 5 : if (nDateComponents >= 4)
2496 : {
2497 3 : osRet += CPLSPrintf("%02d", nHour);
2498 3 : if (nDateComponents >= 5)
2499 3 : osRet += CPLSPrintf(":%02d", nMinute);
2500 3 : if (nDateComponents >= 6)
2501 3 : osRet += CPLSPrintf(":%02d", nSecond);
2502 3 : osRet += "Z";
2503 : }
2504 5 : return osRet;
2505 : }
2506 :
2507 : /************************************************************************/
2508 : /* BuildFilter() */
2509 : /************************************************************************/
2510 :
2511 11 : CPLString OGROAPIFLayer::BuildFilter(const swq_expr_node *poNode)
2512 : {
2513 11 : if (poNode->eNodeType == SNT_OPERATION && poNode->nOperation == SWQ_AND &&
2514 3 : poNode->nSubExprCount == 2)
2515 : {
2516 3 : const auto leftExpr = poNode->papoSubExpr[0];
2517 3 : const auto rightExpr = poNode->papoSubExpr[1];
2518 :
2519 : // Detect expression: datetime >=|> XXX and datetime <=|< XXXX
2520 3 : if (leftExpr->eNodeType == SNT_OPERATION &&
2521 3 : (leftExpr->nOperation == SWQ_GT ||
2522 3 : leftExpr->nOperation == SWQ_GE) &&
2523 1 : leftExpr->nSubExprCount == 2 &&
2524 1 : leftExpr->papoSubExpr[0]->eNodeType == SNT_COLUMN &&
2525 1 : leftExpr->papoSubExpr[1]->eNodeType == SNT_CONSTANT &&
2526 1 : rightExpr->eNodeType == SNT_OPERATION &&
2527 1 : (rightExpr->nOperation == SWQ_LT ||
2528 1 : rightExpr->nOperation == SWQ_LE) &&
2529 1 : rightExpr->nSubExprCount == 2 &&
2530 1 : rightExpr->papoSubExpr[0]->eNodeType == SNT_COLUMN &&
2531 1 : rightExpr->papoSubExpr[1]->eNodeType == SNT_CONSTANT &&
2532 1 : leftExpr->papoSubExpr[0]->field_index ==
2533 1 : rightExpr->papoSubExpr[0]->field_index &&
2534 1 : leftExpr->papoSubExpr[1]->field_type == SWQ_TIMESTAMP &&
2535 1 : rightExpr->papoSubExpr[1]->field_type == SWQ_TIMESTAMP)
2536 : {
2537 1 : const OGRFieldDefn *poFieldDefn = GetLayerDefn()->GetFieldDefn(
2538 1 : leftExpr->papoSubExpr[0]->field_index);
2539 2 : if (poFieldDefn && (poFieldDefn->GetType() == OFTDate ||
2540 1 : poFieldDefn->GetType() == OFTDateTime))
2541 : {
2542 1 : CPLString osExpr;
2543 : {
2544 1 : int nYear = 0, nMonth = 0, nDay = 0, nHour = 0, nMinute = 0,
2545 1 : nSecond = 0;
2546 1 : int nDateComponents = OGRWF3ParseDateTime(
2547 1 : leftExpr->papoSubExpr[1]->string_value, nYear, nMonth,
2548 : nDay, nHour, nMinute, nSecond);
2549 1 : if (nDateComponents >= 3)
2550 : {
2551 : osExpr =
2552 1 : "datetime=" +
2553 2 : SerializeDateTime(nDateComponents, nYear, nMonth,
2554 1 : nDay, nHour, nMinute, nSecond);
2555 : }
2556 : }
2557 1 : if (!osExpr.empty())
2558 : {
2559 1 : int nYear = 0, nMonth = 0, nDay = 0, nHour = 0, nMinute = 0,
2560 1 : nSecond = 0;
2561 1 : int nDateComponents = OGRWF3ParseDateTime(
2562 1 : rightExpr->papoSubExpr[1]->string_value, nYear, nMonth,
2563 : nDay, nHour, nMinute, nSecond);
2564 1 : if (nDateComponents >= 3)
2565 : {
2566 : osExpr +=
2567 : "%2F" // '/' URL encoded
2568 2 : + SerializeDateTime(nDateComponents, nYear, nMonth,
2569 1 : nDay, nHour, nMinute, nSecond);
2570 1 : return osExpr;
2571 : }
2572 : }
2573 : }
2574 : }
2575 :
2576 : // For AND, we can deal with a failure in one of the branch
2577 : // since client-side will do that extra filtering
2578 4 : CPLString osFilter1 = BuildFilter(leftExpr);
2579 4 : CPLString osFilter2 = BuildFilter(rightExpr);
2580 2 : if (!osFilter1.empty() && !osFilter2.empty())
2581 : {
2582 2 : return osFilter1 + "&" + osFilter2;
2583 : }
2584 1 : else if (!osFilter1.empty())
2585 1 : return osFilter1;
2586 : else
2587 0 : return osFilter2;
2588 : }
2589 8 : else if (poNode->eNodeType == SNT_OPERATION &&
2590 8 : poNode->nOperation == SWQ_EQ && poNode->nSubExprCount == 2 &&
2591 5 : poNode->papoSubExpr[0]->eNodeType == SNT_COLUMN &&
2592 5 : poNode->papoSubExpr[1]->eNodeType == SNT_CONSTANT)
2593 : {
2594 5 : const int nFieldIdx = poNode->papoSubExpr[0]->field_index;
2595 : const OGRFieldDefn *poFieldDefn =
2596 5 : GetLayerDefn()->GetFieldDefn(nFieldIdx);
2597 : int nDateComponents;
2598 5 : int nYear = 0, nMonth = 0, nDay = 0, nHour = 0, nMinute = 0,
2599 5 : nSecond = 0;
2600 15 : if (m_bHasStringIdMember &&
2601 6 : strcmp(poFieldDefn->GetNameRef(), "id") == 0 &&
2602 1 : poNode->papoSubExpr[1]->field_type == SWQ_STRING)
2603 : {
2604 1 : m_osGetID = poNode->papoSubExpr[1]->string_value;
2605 : }
2606 8 : else if (poFieldDefn &&
2607 8 : m_aoSetQueryableAttributes.find(poFieldDefn->GetNameRef()) !=
2608 8 : m_aoSetQueryableAttributes.end())
2609 : {
2610 : char *pszEscapedFieldName =
2611 2 : CPLEscapeString(poFieldDefn->GetNameRef(), -1, CPLES_URL);
2612 2 : const CPLString osEscapedFieldName(pszEscapedFieldName);
2613 2 : CPLFree(pszEscapedFieldName);
2614 :
2615 2 : if (poNode->papoSubExpr[1]->field_type == SWQ_STRING)
2616 : {
2617 2 : char *pszEscapedValue = CPLEscapeString(
2618 1 : poNode->papoSubExpr[1]->string_value, -1, CPLES_URL);
2619 2 : CPLString osRet(osEscapedFieldName);
2620 1 : osRet += "=";
2621 1 : osRet += pszEscapedValue;
2622 1 : CPLFree(pszEscapedValue);
2623 1 : return osRet;
2624 : }
2625 1 : if (poNode->papoSubExpr[1]->field_type == SWQ_INTEGER)
2626 : {
2627 2 : CPLString osRet(osEscapedFieldName);
2628 1 : osRet += "=";
2629 : osRet +=
2630 1 : CPLSPrintf("%" PRId64, poNode->papoSubExpr[1]->int_value);
2631 1 : return osRet;
2632 : }
2633 : }
2634 2 : else if (poFieldDefn &&
2635 4 : (poFieldDefn->GetType() == OFTDate ||
2636 2 : poFieldDefn->GetType() == OFTDateTime) &&
2637 5 : poNode->papoSubExpr[1]->field_type == SWQ_TIMESTAMP &&
2638 1 : (nDateComponents = OGRWF3ParseDateTime(
2639 1 : poNode->papoSubExpr[1]->string_value, nYear, nMonth, nDay,
2640 : nHour, nMinute, nSecond)) >= 3)
2641 : {
2642 2 : return "datetime=" + SerializeDateTime(nDateComponents, nYear,
2643 : nMonth, nDay, nHour, nMinute,
2644 1 : nSecond);
2645 2 : }
2646 : }
2647 3 : else if (poNode->eNodeType == SNT_OPERATION &&
2648 3 : (poNode->nOperation == SWQ_GT || poNode->nOperation == SWQ_GE ||
2649 2 : poNode->nOperation == SWQ_LT || poNode->nOperation == SWQ_LE) &&
2650 2 : poNode->nSubExprCount == 2 &&
2651 2 : poNode->papoSubExpr[0]->eNodeType == SNT_COLUMN &&
2652 2 : poNode->papoSubExpr[1]->eNodeType == SNT_CONSTANT &&
2653 2 : poNode->papoSubExpr[1]->field_type == SWQ_TIMESTAMP)
2654 : {
2655 2 : const int nFieldIdx = poNode->papoSubExpr[0]->field_index;
2656 : const OGRFieldDefn *poFieldDefn =
2657 2 : GetLayerDefn()->GetFieldDefn(nFieldIdx);
2658 : int nDateComponents;
2659 2 : int nYear = 0, nMonth = 0, nDay = 0, nHour = 0, nMinute = 0,
2660 2 : nSecond = 0;
2661 2 : if (poFieldDefn &&
2662 4 : (poFieldDefn->GetType() == OFTDate ||
2663 6 : poFieldDefn->GetType() == OFTDateTime) &&
2664 2 : (nDateComponents = OGRWF3ParseDateTime(
2665 2 : poNode->papoSubExpr[1]->string_value, nYear, nMonth, nDay,
2666 : nHour, nMinute, nSecond)) >= 3)
2667 : {
2668 : CPLString osDT(SerializeDateTime(nDateComponents, nYear, nMonth,
2669 4 : nDay, nHour, nMinute, nSecond));
2670 2 : if (poNode->nOperation == SWQ_GT || poNode->nOperation == SWQ_GE)
2671 : {
2672 2 : return "datetime=" + osDT + "%2F..";
2673 : }
2674 : else
2675 : {
2676 2 : return "datetime=..%2F" + osDT;
2677 : }
2678 : }
2679 : }
2680 3 : m_bFilterMustBeClientSideEvaluated = true;
2681 3 : return CPLString();
2682 : }
2683 :
2684 : /************************************************************************/
2685 : /* BuildFilterCQLText() */
2686 : /************************************************************************/
2687 :
2688 0 : CPLString OGROAPIFLayer::BuildFilterCQLText(const swq_expr_node *poNode)
2689 : {
2690 0 : if (poNode->eNodeType == SNT_OPERATION && poNode->nOperation == SWQ_AND &&
2691 0 : poNode->nSubExprCount == 2)
2692 : {
2693 0 : const auto leftExpr = poNode->papoSubExpr[0];
2694 0 : const auto rightExpr = poNode->papoSubExpr[1];
2695 :
2696 : // For AND, we can deal with a failure in one of the branch
2697 : // since client-side will do that extra filtering
2698 0 : CPLString osFilter1 = BuildFilterCQLText(leftExpr);
2699 0 : CPLString osFilter2 = BuildFilterCQLText(rightExpr);
2700 0 : if (!osFilter1.empty() && !osFilter2.empty())
2701 : {
2702 0 : return '(' + osFilter1 + ") AND (" + osFilter2 + ')';
2703 : }
2704 0 : else if (!osFilter1.empty())
2705 0 : return osFilter1;
2706 : else
2707 0 : return osFilter2;
2708 : }
2709 0 : else if (poNode->eNodeType == SNT_OPERATION &&
2710 0 : poNode->nOperation == SWQ_OR && poNode->nSubExprCount == 2)
2711 : {
2712 0 : const auto leftExpr = poNode->papoSubExpr[0];
2713 0 : const auto rightExpr = poNode->papoSubExpr[1];
2714 :
2715 0 : CPLString osFilter1 = BuildFilterCQLText(leftExpr);
2716 0 : CPLString osFilter2 = BuildFilterCQLText(rightExpr);
2717 0 : if (!osFilter1.empty() && !osFilter2.empty())
2718 : {
2719 0 : return '(' + osFilter1 + ") OR (" + osFilter2 + ')';
2720 0 : }
2721 : }
2722 0 : else if (poNode->eNodeType == SNT_OPERATION &&
2723 0 : poNode->nOperation == SWQ_NOT && poNode->nSubExprCount == 1)
2724 : {
2725 0 : const auto childExpr = poNode->papoSubExpr[0];
2726 0 : CPLString osFilterChild = BuildFilterCQLText(childExpr);
2727 0 : if (!osFilterChild.empty())
2728 : {
2729 0 : return "NOT (" + osFilterChild + ')';
2730 0 : }
2731 : }
2732 0 : else if (poNode->eNodeType == SNT_OPERATION &&
2733 0 : poNode->nOperation == SWQ_ISNULL && poNode->nSubExprCount == 1 &&
2734 0 : poNode->papoSubExpr[0]->eNodeType == SNT_COLUMN)
2735 : {
2736 0 : const int nFieldIdx = poNode->papoSubExpr[0]->field_index;
2737 : const OGRFieldDefn *poFieldDefn =
2738 0 : GetLayerDefn()->GetFieldDefn(nFieldIdx);
2739 0 : if (poFieldDefn)
2740 : {
2741 0 : return CPLString("(") + poFieldDefn->GetNameRef() + " IS NULL)";
2742 0 : }
2743 : }
2744 0 : else if (poNode->eNodeType == SNT_OPERATION &&
2745 0 : (poNode->nOperation == SWQ_EQ || poNode->nOperation == SWQ_NE ||
2746 0 : poNode->nOperation == SWQ_GT || poNode->nOperation == SWQ_GE ||
2747 0 : poNode->nOperation == SWQ_LT || poNode->nOperation == SWQ_LE ||
2748 0 : poNode->nOperation == SWQ_LIKE ||
2749 0 : poNode->nOperation == SWQ_ILIKE) &&
2750 0 : poNode->nSubExprCount == 2 &&
2751 0 : poNode->papoSubExpr[0]->eNodeType == SNT_COLUMN &&
2752 0 : poNode->papoSubExpr[1]->eNodeType == SNT_CONSTANT)
2753 : {
2754 0 : const int nFieldIdx = poNode->papoSubExpr[0]->field_index;
2755 : const OGRFieldDefn *poFieldDefn =
2756 0 : GetLayerDefn()->GetFieldDefn(nFieldIdx);
2757 0 : if (m_bHasStringIdMember && poNode->nOperation == SWQ_EQ &&
2758 0 : strcmp(poFieldDefn->GetNameRef(), "id") == 0 &&
2759 0 : poNode->papoSubExpr[1]->field_type == SWQ_STRING)
2760 : {
2761 0 : m_osGetID = poNode->papoSubExpr[1]->string_value;
2762 : }
2763 0 : else if (poFieldDefn &&
2764 0 : m_aoSetQueryableAttributes.find(poFieldDefn->GetNameRef()) !=
2765 0 : m_aoSetQueryableAttributes.end())
2766 : {
2767 0 : CPLString osRet(poFieldDefn->GetNameRef());
2768 0 : switch (poNode->nOperation)
2769 : {
2770 0 : case SWQ_EQ:
2771 0 : osRet += " = ";
2772 0 : break;
2773 0 : case SWQ_NE:
2774 0 : osRet += " <> ";
2775 0 : break;
2776 0 : case SWQ_GT:
2777 0 : osRet += " > ";
2778 0 : break;
2779 0 : case SWQ_GE:
2780 0 : osRet += " >= ";
2781 0 : break;
2782 0 : case SWQ_LT:
2783 0 : osRet += " < ";
2784 0 : break;
2785 0 : case SWQ_LE:
2786 0 : osRet += " <= ";
2787 0 : break;
2788 0 : case SWQ_LIKE:
2789 0 : osRet += " LIKE ";
2790 0 : break;
2791 0 : case SWQ_ILIKE:
2792 0 : osRet += " ILIKE ";
2793 0 : break;
2794 0 : default:
2795 0 : CPLAssert(false);
2796 : break;
2797 : }
2798 0 : if (poNode->papoSubExpr[1]->field_type == SWQ_STRING)
2799 : {
2800 0 : osRet += '\'';
2801 0 : osRet += CPLString(poNode->papoSubExpr[1]->string_value)
2802 0 : .replaceAll('\'', "''");
2803 0 : osRet += '\'';
2804 0 : return osRet;
2805 : }
2806 0 : if (poNode->papoSubExpr[1]->field_type == SWQ_INTEGER ||
2807 0 : poNode->papoSubExpr[1]->field_type == SWQ_INTEGER64)
2808 : {
2809 : osRet +=
2810 0 : CPLSPrintf("%" PRId64, poNode->papoSubExpr[1]->int_value);
2811 0 : return osRet;
2812 : }
2813 0 : if (poNode->papoSubExpr[1]->field_type == SWQ_FLOAT)
2814 : {
2815 : osRet +=
2816 0 : CPLSPrintf("%.16g", poNode->papoSubExpr[1]->float_value);
2817 0 : return osRet;
2818 : }
2819 0 : if (poNode->papoSubExpr[1]->field_type == SWQ_TIMESTAMP)
2820 : {
2821 : int nDateComponents;
2822 0 : int nYear = 0, nMonth = 0, nDay = 0, nHour = 0, nMinute = 0,
2823 0 : nSecond = 0;
2824 0 : if ((poFieldDefn->GetType() == OFTDate ||
2825 0 : poFieldDefn->GetType() == OFTDateTime) &&
2826 0 : (nDateComponents = OGRWF3ParseDateTime(
2827 0 : poNode->papoSubExpr[1]->string_value, nYear, nMonth,
2828 : nDay, nHour, nMinute, nSecond)) >= 3)
2829 : {
2830 : CPLString osDT(SerializeDateTime(nDateComponents, nYear,
2831 : nMonth, nDay, nHour,
2832 0 : nMinute, nSecond));
2833 0 : osRet += '\'';
2834 0 : osRet += osDT;
2835 0 : osRet += '\'';
2836 0 : return osRet;
2837 : }
2838 : }
2839 : }
2840 : }
2841 :
2842 0 : m_bFilterMustBeClientSideEvaluated = true;
2843 0 : return CPLString();
2844 : }
2845 :
2846 : /************************************************************************/
2847 : /* BuildFilterJSONFilterExpr() */
2848 : /************************************************************************/
2849 :
2850 0 : CPLString OGROAPIFLayer::BuildFilterJSONFilterExpr(const swq_expr_node *poNode)
2851 : {
2852 0 : if (poNode->eNodeType == SNT_OPERATION && poNode->nOperation == SWQ_AND &&
2853 0 : poNode->nSubExprCount == 2)
2854 : {
2855 0 : const auto leftExpr = poNode->papoSubExpr[0];
2856 0 : const auto rightExpr = poNode->papoSubExpr[1];
2857 :
2858 : // For AND, we can deal with a failure in one of the branch
2859 : // since client-side will do that extra filtering
2860 0 : CPLString osFilter1 = BuildFilterJSONFilterExpr(leftExpr);
2861 0 : CPLString osFilter2 = BuildFilterJSONFilterExpr(rightExpr);
2862 0 : if (!osFilter1.empty() && !osFilter2.empty())
2863 : {
2864 0 : return "[\"all\"," + osFilter1 + ',' + osFilter2 + ']';
2865 : }
2866 0 : else if (!osFilter1.empty())
2867 0 : return osFilter1;
2868 : else
2869 0 : return osFilter2;
2870 : }
2871 0 : else if (poNode->eNodeType == SNT_OPERATION &&
2872 0 : poNode->nOperation == SWQ_OR && poNode->nSubExprCount == 2)
2873 : {
2874 0 : const auto leftExpr = poNode->papoSubExpr[0];
2875 0 : const auto rightExpr = poNode->papoSubExpr[1];
2876 :
2877 0 : CPLString osFilter1 = BuildFilterJSONFilterExpr(leftExpr);
2878 0 : CPLString osFilter2 = BuildFilterJSONFilterExpr(rightExpr);
2879 0 : if (!osFilter1.empty() && !osFilter2.empty())
2880 : {
2881 0 : return "[\"any\"," + osFilter1 + ',' + osFilter2 + ']';
2882 0 : }
2883 : }
2884 0 : else if (poNode->eNodeType == SNT_OPERATION &&
2885 0 : poNode->nOperation == SWQ_NOT && poNode->nSubExprCount == 1)
2886 : {
2887 0 : const auto childExpr = poNode->papoSubExpr[0];
2888 0 : CPLString osFilterChild = BuildFilterJSONFilterExpr(childExpr);
2889 0 : if (!osFilterChild.empty())
2890 : {
2891 0 : return "[\"!\"," + osFilterChild + ']';
2892 0 : }
2893 : }
2894 0 : else if (poNode->eNodeType == SNT_OPERATION &&
2895 0 : poNode->nOperation == SWQ_ISNULL && poNode->nSubExprCount == 1)
2896 : {
2897 0 : const auto childExpr = poNode->papoSubExpr[0];
2898 0 : CPLString osFilterChild = BuildFilterJSONFilterExpr(childExpr);
2899 0 : if (!osFilterChild.empty())
2900 : {
2901 0 : return "[\"==\"," + osFilterChild + ",null]";
2902 0 : }
2903 : }
2904 0 : else if (poNode->eNodeType == SNT_OPERATION &&
2905 0 : (poNode->nOperation == SWQ_EQ || poNode->nOperation == SWQ_NE ||
2906 0 : poNode->nOperation == SWQ_GT || poNode->nOperation == SWQ_GE ||
2907 0 : poNode->nOperation == SWQ_LT || poNode->nOperation == SWQ_LE ||
2908 0 : poNode->nOperation == SWQ_LIKE) &&
2909 0 : poNode->nSubExprCount == 2)
2910 : {
2911 0 : if (m_bHasStringIdMember && poNode->nOperation == SWQ_EQ &&
2912 0 : poNode->papoSubExpr[0]->eNodeType == SNT_COLUMN &&
2913 0 : poNode->papoSubExpr[1]->eNodeType == SNT_CONSTANT &&
2914 0 : poNode->papoSubExpr[1]->field_type == SWQ_STRING)
2915 : {
2916 0 : const int nFieldIdx = poNode->papoSubExpr[0]->field_index;
2917 : const OGRFieldDefn *poFieldDefn =
2918 0 : GetLayerDefn()->GetFieldDefn(nFieldIdx);
2919 0 : if (strcmp(poFieldDefn->GetNameRef(), "id") == 0)
2920 : {
2921 0 : m_osGetID = poNode->papoSubExpr[1]->string_value;
2922 0 : return CPLString();
2923 : }
2924 : }
2925 :
2926 0 : CPLString osRet("[\"");
2927 0 : switch (poNode->nOperation)
2928 : {
2929 0 : case SWQ_EQ:
2930 0 : osRet += "==";
2931 0 : break;
2932 0 : case SWQ_NE:
2933 0 : osRet += "!=";
2934 0 : break;
2935 0 : case SWQ_GT:
2936 0 : osRet += ">";
2937 0 : break;
2938 0 : case SWQ_GE:
2939 0 : osRet += ">=";
2940 0 : break;
2941 0 : case SWQ_LT:
2942 0 : osRet += "<";
2943 0 : break;
2944 0 : case SWQ_LE:
2945 0 : osRet += "<=";
2946 0 : break;
2947 0 : case SWQ_LIKE:
2948 0 : osRet += "like";
2949 0 : break;
2950 0 : default:
2951 0 : CPLAssert(false);
2952 : break;
2953 : }
2954 0 : osRet += "\",";
2955 0 : CPLString osFilter1 = BuildFilterJSONFilterExpr(poNode->papoSubExpr[0]);
2956 0 : CPLString osFilter2 = BuildFilterJSONFilterExpr(poNode->papoSubExpr[1]);
2957 0 : if (!osFilter1.empty() && !osFilter2.empty())
2958 : {
2959 0 : osRet += osFilter1;
2960 0 : osRet += ',';
2961 0 : osRet += osFilter2;
2962 0 : osRet += ']';
2963 0 : return osRet;
2964 0 : }
2965 : }
2966 0 : else if (poNode->eNodeType == SNT_COLUMN)
2967 : {
2968 0 : const int nFieldIdx = poNode->field_index;
2969 : const OGRFieldDefn *poFieldDefn =
2970 0 : GetLayerDefn()->GetFieldDefn(nFieldIdx);
2971 0 : if (poFieldDefn &&
2972 0 : m_aoSetQueryableAttributes.find(poFieldDefn->GetNameRef()) !=
2973 0 : m_aoSetQueryableAttributes.end())
2974 : {
2975 0 : CPLString osRet("[\"get\",\"");
2976 0 : osRet += CPLString(poFieldDefn->GetNameRef())
2977 0 : .replaceAll('\\', "\\\\")
2978 0 : .replaceAll('"', "\\\"");
2979 0 : osRet += "\"]";
2980 0 : return osRet;
2981 : }
2982 : }
2983 0 : else if (poNode->eNodeType == SNT_CONSTANT)
2984 : {
2985 0 : if (poNode->field_type == SWQ_STRING)
2986 : {
2987 0 : CPLString osRet("\"");
2988 0 : osRet += CPLString(poNode->string_value)
2989 0 : .replaceAll('\\', "\\\\")
2990 0 : .replaceAll('"', "\\\"");
2991 0 : osRet += '"';
2992 0 : return osRet;
2993 : }
2994 0 : if (poNode->field_type == SWQ_INTEGER ||
2995 0 : poNode->field_type == SWQ_INTEGER64)
2996 : {
2997 0 : return CPLSPrintf("%" PRId64, poNode->int_value);
2998 : }
2999 0 : if (poNode->field_type == SWQ_FLOAT)
3000 : {
3001 0 : return CPLSPrintf("%.16g", poNode->float_value);
3002 : }
3003 0 : if (poNode->field_type == SWQ_TIMESTAMP)
3004 : {
3005 0 : int nYear = 0, nMonth = 0, nDay = 0, nHour = 0, nMinute = 0,
3006 0 : nSecond = 0;
3007 : const int nDateComponents =
3008 0 : OGRWF3ParseDateTime(poNode->string_value, nYear, nMonth, nDay,
3009 : nHour, nMinute, nSecond);
3010 0 : if (nDateComponents >= 3)
3011 : {
3012 : CPLString osDT(SerializeDateTime(nDateComponents, nYear, nMonth,
3013 : nDay, nHour, nMinute,
3014 0 : nSecond));
3015 0 : CPLString osRet("\"");
3016 0 : osRet += osDT;
3017 0 : osRet += '"';
3018 0 : return osRet;
3019 : }
3020 : }
3021 : }
3022 :
3023 0 : m_bFilterMustBeClientSideEvaluated = true;
3024 0 : return CPLString();
3025 : }
3026 :
3027 : /************************************************************************/
3028 : /* GetQueryableAttributes() */
3029 : /************************************************************************/
3030 :
3031 7 : void OGROAPIFLayer::GetQueryableAttributes()
3032 : {
3033 7 : if (m_bGotQueryableAttributes)
3034 6 : return;
3035 1 : m_bGotQueryableAttributes = true;
3036 1 : std::string osAPIURL;
3037 1 : CPLJSONDocument oAPIDoc = m_poDS->GetAPIDoc(osAPIURL);
3038 1 : if (oAPIDoc.GetRoot().GetString("openapi").empty())
3039 0 : return;
3040 :
3041 3 : CPLJSONObject oPaths = oAPIDoc.GetRoot().GetObj("paths");
3042 : CPLJSONArray oParameters =
3043 3 : oPaths.GetObj(m_osPath).GetObj("get").GetArray("parameters");
3044 1 : if (!oParameters.IsValid())
3045 : {
3046 0 : oParameters = oPaths.GetObj("/collections/{collectionId}/items")
3047 0 : .GetObj("get")
3048 0 : .GetArray("parameters");
3049 : }
3050 3 : for (int i = 0; i < oParameters.Size(); i++)
3051 : {
3052 4 : CPLJSONObject oParam = oParameters[i];
3053 6 : CPLString osRef = oParam.GetString("$ref");
3054 2 : if (!osRef.empty() && osRef.find("#/") == 0)
3055 : {
3056 0 : oParam = oAPIDoc.GetRoot().GetObj(osRef.substr(2));
3057 : }
3058 2 : if (oParam.GetString("in") == "query")
3059 : {
3060 6 : const auto osName(oParam.GetString("name"));
3061 2 : if (osName == "filter-lang")
3062 : {
3063 0 : const auto oEnums = oParam.GetObj("schema").GetArray("enum");
3064 0 : for (int j = 0; j < oEnums.Size(); j++)
3065 : {
3066 0 : if (oEnums[j].ToString() == "cql-text")
3067 : {
3068 0 : m_bHasCQLText = true;
3069 0 : CPLDebug("OAPIF", "CQL text detected");
3070 : }
3071 0 : else if (oEnums[j].ToString() == "json-filter-expr")
3072 : {
3073 0 : m_bHasJSONFilterExpression = true;
3074 0 : CPLDebug("OAPIF", "JSON Filter expression detected");
3075 : }
3076 : }
3077 : }
3078 2 : else if (GetLayerDefn()->GetFieldIndex(osName.c_str()) >= 0)
3079 : {
3080 2 : m_aoSetQueryableAttributes.insert(osName);
3081 : }
3082 : }
3083 : }
3084 :
3085 : // HACK
3086 1 : if (CPLTestBool(CPLGetConfigOption("OGR_OAPIF_ALLOW_CQL_TEXT", "NO")))
3087 0 : m_bHasCQLText = true;
3088 :
3089 1 : if (m_bHasCQLText || m_bHasJSONFilterExpression)
3090 : {
3091 0 : if (!m_osQueryablesURL.empty())
3092 : {
3093 0 : CPLJSONDocument oDoc;
3094 0 : if (m_poDS->DownloadJSon(m_osQueryablesURL, oDoc))
3095 : {
3096 0 : auto oQueryables = oDoc.GetRoot().GetArray("queryables");
3097 0 : for (int i = 0; i < oQueryables.Size(); i++)
3098 : {
3099 0 : const auto osId = oQueryables[i].GetString("id");
3100 0 : if (!osId.empty())
3101 : {
3102 0 : m_aoSetQueryableAttributes.insert(osId);
3103 : }
3104 : }
3105 : }
3106 : }
3107 : }
3108 : }
3109 :
3110 : /************************************************************************/
3111 : /* SetAttributeFilter() */
3112 : /************************************************************************/
3113 :
3114 9 : OGRErr OGROAPIFLayer::SetAttributeFilter(const char *pszQuery)
3115 :
3116 : {
3117 9 : if (m_poAttrQuery == nullptr && pszQuery == nullptr)
3118 1 : return OGRERR_NONE;
3119 :
3120 8 : if (!m_bFeatureDefnEstablished)
3121 1 : EstablishFeatureDefn();
3122 :
3123 8 : OGRErr eErr = OGRLayer::SetAttributeFilter(pszQuery);
3124 :
3125 8 : m_osAttributeFilter.clear();
3126 8 : m_bFilterMustBeClientSideEvaluated = false;
3127 8 : m_osGetID.clear();
3128 8 : if (m_poAttrQuery != nullptr)
3129 : {
3130 7 : GetQueryableAttributes();
3131 :
3132 7 : swq_expr_node *poNode = (swq_expr_node *)m_poAttrQuery->GetSWQExpr();
3133 :
3134 7 : poNode->ReplaceBetweenByGEAndLERecurse();
3135 :
3136 7 : if (m_bHasCQLText)
3137 : {
3138 0 : m_osAttributeFilter = BuildFilterCQLText(poNode);
3139 0 : if (!m_osAttributeFilter.empty())
3140 : {
3141 : char *pszEscaped =
3142 0 : CPLEscapeString(m_osAttributeFilter, -1, CPLES_URL);
3143 0 : m_osAttributeFilter = "filter=";
3144 0 : m_osAttributeFilter += pszEscaped;
3145 0 : m_osAttributeFilter += "&filter-lang=cql-text";
3146 0 : CPLFree(pszEscaped);
3147 : }
3148 : }
3149 7 : else if (m_bHasJSONFilterExpression)
3150 : {
3151 0 : m_osAttributeFilter = BuildFilterJSONFilterExpr(poNode);
3152 0 : if (!m_osAttributeFilter.empty())
3153 : {
3154 : char *pszEscaped =
3155 0 : CPLEscapeString(m_osAttributeFilter, -1, CPLES_URL);
3156 0 : m_osAttributeFilter = "filter=";
3157 0 : m_osAttributeFilter += pszEscaped;
3158 0 : m_osAttributeFilter += "&filter-lang=json-filter-expr";
3159 0 : CPLFree(pszEscaped);
3160 : }
3161 : }
3162 : else
3163 : {
3164 7 : m_osAttributeFilter = BuildFilter(poNode);
3165 : }
3166 7 : if (m_osAttributeFilter.empty())
3167 : {
3168 2 : CPLDebug("OAPIF", "Full filter will be evaluated on client side.");
3169 : }
3170 5 : else if (m_bFilterMustBeClientSideEvaluated)
3171 : {
3172 1 : CPLDebug(
3173 : "OAPIF",
3174 : "Only part of the filter will be evaluated on server side.");
3175 : }
3176 : }
3177 :
3178 8 : ResetReading();
3179 :
3180 8 : return eErr;
3181 : }
3182 :
3183 : /************************************************************************/
3184 : /* TestCapability() */
3185 : /************************************************************************/
3186 :
3187 11 : int OGROAPIFLayer::TestCapability(const char *pszCap)
3188 : {
3189 11 : if (EQUAL(pszCap, OLCFastFeatureCount))
3190 : {
3191 3 : return m_nTotalFeatureCount >= 0 && m_poFilterGeom == nullptr &&
3192 3 : m_poAttrQuery == nullptr;
3193 : }
3194 9 : if (EQUAL(pszCap, OLCFastGetExtent))
3195 : {
3196 1 : return m_oOriginalExtent.IsInit();
3197 : }
3198 8 : if (EQUAL(pszCap, OLCStringsAsUTF8))
3199 : {
3200 7 : return TRUE;
3201 : }
3202 : // Don't advertise OLCRandomRead as it requires a GET per feature
3203 1 : return FALSE;
3204 : }
3205 :
3206 : /************************************************************************/
3207 : /* Open() */
3208 : /************************************************************************/
3209 :
3210 41 : static GDALDataset *OGROAPIFDriverOpen(GDALOpenInfo *poOpenInfo)
3211 :
3212 : {
3213 41 : if (!OGROAPIFDriverIdentify(poOpenInfo) || poOpenInfo->eAccess == GA_Update)
3214 0 : return nullptr;
3215 82 : auto poDataset = std::make_unique<OGROAPIFDataset>();
3216 41 : if (!poDataset->Open(poOpenInfo))
3217 9 : return nullptr;
3218 32 : return poDataset.release();
3219 : }
3220 :
3221 : /************************************************************************/
3222 : /* RegisterOGROAPIF() */
3223 : /************************************************************************/
3224 :
3225 1682 : void RegisterOGROAPIF()
3226 :
3227 : {
3228 1682 : if (GDALGetDriverByName("OAPIF") != nullptr)
3229 301 : return;
3230 :
3231 1381 : GDALDriver *poDriver = new GDALDriver();
3232 :
3233 1381 : poDriver->SetDescription("OAPIF");
3234 1381 : poDriver->SetMetadataItem(GDAL_DCAP_VECTOR, "YES");
3235 1381 : poDriver->SetMetadataItem(GDAL_DMD_LONGNAME, "OGC API - Features");
3236 1381 : poDriver->SetMetadataItem(GDAL_DMD_HELPTOPIC, "drivers/vector/oapif.html");
3237 :
3238 1381 : poDriver->SetMetadataItem(GDAL_DMD_CONNECTION_PREFIX, "OAPIF:");
3239 1381 : poDriver->SetMetadataItem(GDAL_DMD_SUPPORTED_SQL_DIALECTS, "OGRSQL SQLITE");
3240 :
3241 1381 : poDriver->SetMetadataItem(
3242 : GDAL_DMD_OPENOPTIONLIST,
3243 : "<OpenOptionList>"
3244 : " <Option name='URL' type='string' "
3245 : "description='URL to the landing page or a /collections/{id}' "
3246 : "required='true'/>"
3247 : " <Option name='PAGE_SIZE' type='int' "
3248 : "description='Maximum number of features to retrieve in a single "
3249 : "request'/>"
3250 : " <Option name='INITIAL_REQUEST_PAGE_SIZE' type='int' "
3251 : "description='Maximum number of features to retrieve in the initial "
3252 : "request issued to determine the schema from a feature sample'/>"
3253 : " <Option name='USERPWD' type='string' "
3254 : "description='Basic authentication as username:password'/>"
3255 : " <Option name='IGNORE_SCHEMA' type='boolean' "
3256 : "description='Whether the XML Schema or JSON Schema should be ignored' "
3257 : "default='NO'/>"
3258 : " <Option name='CRS' type='string' "
3259 : "description='CRS identifier to use for layers'/>"
3260 : " <Option name='PREFERRED_CRS' type='string' "
3261 : "description='Preferred CRS identifier to use for layers'/>"
3262 : " <Option name='SERVER_FEATURE_AXIS_ORDER' type='string-select' "
3263 : "description='Coordinate axis order of GeoJSON features returned by "
3264 : "the server' "
3265 : "default='AUTHORITY_COMPLIANT'>"
3266 : " <Value>AUTHORITY_COMPLIANT</Value>"
3267 : " <Value>GIS_FRIENDLY</Value>"
3268 : " </Option>"
3269 : " <Option name='DATETIME' type='string' "
3270 : "description=\"Date-time filter to pass to items requests with the "
3271 : "'datetime' parameter\"/>"
3272 1381 : "</OpenOptionList>");
3273 :
3274 1381 : poDriver->pfnIdentify = OGROAPIFDriverIdentify;
3275 1381 : poDriver->pfnOpen = OGROAPIFDriverOpen;
3276 :
3277 1381 : GetGDALDriverManager()->RegisterDriver(poDriver);
3278 : }
|