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