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