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