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