Skip to content

Commit 258b688

Browse files
authored
Fix handling of relative STAC links (qgis#59199)
1 parent 8a93c2d commit 258b688

File tree

5 files changed

+76
-15
lines changed

5 files changed

+76
-15
lines changed

src/core/stac/qgsstaccontroller.cpp

+7
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ void QgsStacController::handleStacObjectReply()
9999
const QByteArray data = reply->readAll();
100100
QgsStacParser parser;
101101
parser.setData( data );
102+
parser.setBaseUrl( reply->url() );
102103

103104
QgsStacObject *object = nullptr;
104105
switch ( parser.type() )
@@ -142,6 +143,7 @@ void QgsStacController::handleItemCollectionReply()
142143
const QByteArray data = reply->readAll();
143144
QgsStacParser parser;
144145
parser.setData( data );
146+
parser.setBaseUrl( reply->url() );
145147

146148
QgsStacItemCollection *fc = parser.itemCollection();
147149
mFetchedItemCollections.insert( requestId, fc );
@@ -176,6 +178,7 @@ QgsStacObject *QgsStacController::fetchStacObject( const QUrl &url, QString *err
176178

177179
QgsStacParser parser;
178180
parser.setData( data );
181+
parser.setBaseUrl( url );
179182
QgsStacObject *object = nullptr;
180183
switch ( parser.type() )
181184
{
@@ -215,6 +218,7 @@ QgsStacItemCollection *QgsStacController::fetchItemCollection( const QUrl &url,
215218

216219
QgsStacParser parser;
217220
parser.setData( data );
221+
parser.setBaseUrl( url );
218222
QgsStacItemCollection *ic = parser.itemCollection();
219223

220224
if ( error )
@@ -287,6 +291,7 @@ QgsStacCatalog *QgsStacController::openLocalCatalog( const QString &fileName ) c
287291

288292
QgsStacParser parser;
289293
parser.setData( file.readAll() );
294+
parser.setBaseUrl( fileName );
290295
return parser.catalog();
291296
}
292297

@@ -303,6 +308,7 @@ QgsStacCollection *QgsStacController::openLocalCollection( const QString &fileNa
303308

304309
QgsStacParser parser;
305310
parser.setData( file.readAll() );
311+
parser.setBaseUrl( fileName );
306312
return parser.collection();
307313
}
308314

@@ -318,6 +324,7 @@ QgsStacItem *QgsStacController::openLocalItem( const QString &fileName ) const
318324

319325
QgsStacParser parser;
320326
parser.setData( file.readAll() );
327+
parser.setBaseUrl( fileName );
321328
return parser.item();
322329
}
323330

src/core/stac/qgsstacparser.cpp

+15-2
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ void QgsStacParser::setData( const QByteArray &data )
5454
}
5555
}
5656

57+
void QgsStacParser::setBaseUrl( const QUrl &url )
58+
{
59+
mBaseUrl = url;
60+
}
61+
5762
QgsStacObject::Type QgsStacParser::type() const
5863
{
5964
return mType;
@@ -390,7 +395,11 @@ QVector<QgsStacLink> QgsStacParser::parseLinks( const json &data )
390395
links.reserve( static_cast<int>( data.size() ) );
391396
for ( const auto &link : data )
392397
{
393-
const QgsStacLink l( QString::fromStdString( link.at( "href" ) ),
398+
QUrl linkUrl( QString::fromStdString( link.at( "href" ) ) );
399+
if ( linkUrl.isRelative() )
400+
linkUrl = mBaseUrl.resolved( linkUrl );
401+
402+
const QgsStacLink l( linkUrl.toString(),
394403
QString::fromStdString( link.at( "rel" ) ),
395404
link.contains( "type" ) ? getString( link["type"] ) : QString(),
396405
link.contains( "title" ) ? getString( link["title"] ) : QString() );
@@ -405,7 +414,11 @@ QMap<QString, QgsStacAsset> QgsStacParser::parseAssets( const json &data )
405414
for ( const auto &asset : data.items() )
406415
{
407416
const json value = asset.value();
408-
const QgsStacAsset a( QString::fromStdString( value.at( "href" ) ),
417+
QUrl assetUrl( QString::fromStdString( value.at( "href" ) ) );
418+
if ( assetUrl.isRelative() )
419+
assetUrl = mBaseUrl.resolved( assetUrl );
420+
421+
const QgsStacAsset a( assetUrl.toString(),
409422
value.contains( "title" ) ? getString( value["title"] ) : QString(),
410423
value.contains( "description" ) ? getString( value["description"] ) : QString(),
411424
value.contains( "type" ) ? getString( value["type"] ) : QString(),

src/core/stac/qgsstacparser.h

+10-3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#define SIP_NO_FILE
2020

2121
#include <nlohmann/json.hpp>
22+
#include <QUrl>
2223

2324
#include "qgsstacobject.h"
2425
#include "qgsstacasset.h"
@@ -47,6 +48,12 @@ class QgsStacParser
4748
//! Sets the JSON \data to be parsed
4849
void setData( const QByteArray &data );
4950

51+
/**
52+
* Sets the base \a url that will be used to resolve relative links.
53+
* If not called, relative links will not be resolved to absolute links.
54+
*/
55+
void setBaseUrl( const QUrl &url );
56+
5057
/**
5158
* Returns the parsed STAC Catalog
5259
* If parsing failed, NULLPTR is returned
@@ -93,8 +100,8 @@ class QgsStacParser
93100
QgsStacCatalog *parseCatalog( const nlohmann::json &data );
94101
QgsStacCollection *parseCollection( const nlohmann::json &data );
95102

96-
static QVector< QgsStacLink > parseLinks( const nlohmann::json &data );
97-
static QMap< QString, QgsStacAsset > parseAssets( const nlohmann::json &data );
103+
QVector< QgsStacLink > parseLinks( const nlohmann::json &data );
104+
QMap< QString, QgsStacAsset > parseAssets( const nlohmann::json &data );
98105
static bool isSupportedStacVersion( const QString &version );
99106
//! Returns a QString, treating null elements as empty strings
100107
static QString getString( const nlohmann::json &data );
@@ -103,7 +110,7 @@ class QgsStacParser
103110
QgsStacObject::Type mType = QgsStacObject::Type::Unknown;
104111
std::unique_ptr<QgsStacObject> mObject;
105112
QString mError;
106-
113+
QUrl mBaseUrl;
107114
};
108115

109116

tests/src/core/testqgsstac.cpp

+43-9
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,9 @@ void TestQgsStac::cleanupTestCase()
7373

7474
void TestQgsStac::testParseLocalCatalog()
7575
{
76-
const QString fileName = mDataDir + QStringLiteral( "catalog.json" );
76+
const QUrl url( QStringLiteral( "file://%1%2" ).arg( mDataDir, QStringLiteral( "catalog.json" ) ) );
7777
QgsStacController c;
78-
QgsStacObject *obj = c.fetchStacObject( QStringLiteral( "file://%1" ).arg( fileName ) );
78+
QgsStacObject *obj = c.fetchStacObject( url.toString() );
7979
QVERIFY( obj );
8080
QCOMPARE( obj->type(), QgsStacObject::Type::Catalog );
8181
QgsStacCatalog *cat = dynamic_cast< QgsStacCatalog * >( obj );
@@ -85,17 +85,27 @@ void TestQgsStac::testParseLocalCatalog()
8585
QCOMPARE( cat->stacVersion(), QLatin1String( "1.0.0" ) );
8686
QCOMPARE( cat->title(), QLatin1String( "Example Catalog" ) );
8787
QCOMPARE( cat->description(), QLatin1String( "This catalog is a simple demonstration of an example catalog that is used to organize a hierarchy of collections and their items." ) );
88-
QCOMPARE( cat->links().size(), 6 );
8988
QVERIFY( cat->stacExtensions().isEmpty() );
9089

90+
// check that relative links are correctly resolved into absolute links
91+
const QVector<QgsStacLink> links = cat->links();
92+
QCOMPARE( links.size(), 6 );
93+
const QString basePath = url.adjusted( QUrl::RemoveFilename ).toString();
94+
QCOMPARE( links.at( 0 ).href(), QStringLiteral( "%1catalog.json" ).arg( basePath ) );
95+
QCOMPARE( links.at( 1 ).href(), QStringLiteral( "%1extensions-collection/collection.json" ).arg( basePath ) );
96+
QCOMPARE( links.at( 2 ).href(), QStringLiteral( "%1collection-only/collection.json" ).arg( basePath ) );
97+
QCOMPARE( links.at( 3 ).href(), QStringLiteral( "%1collection-only/collection-with-schemas.json" ).arg( basePath ) );
98+
QCOMPARE( links.at( 4 ).href(), QStringLiteral( "%1collectionless-item.json" ).arg( basePath ) );
99+
QCOMPARE( links.at( 5 ).href(), QStringLiteral( "https://raw.githubusercontent.com/radiantearth/stac-spec/v1.0.0/examples/catalog.json" ) );
100+
91101
delete cat;
92102
}
93103

94104
void TestQgsStac::testParseLocalCollection()
95105
{
96-
const QString fileName = mDataDir + QStringLiteral( "collection.json" );
106+
const QUrl url( QStringLiteral( "file://%1%2" ).arg( mDataDir, QStringLiteral( "collection.json" ) ) );
97107
QgsStacController c;
98-
QgsStacObject *obj = c.fetchStacObject( QStringLiteral( "file://%1" ).arg( fileName ) );
108+
QgsStacObject *obj = c.fetchStacObject( url.toString() );
99109
QVERIFY( obj );
100110
QCOMPARE( obj->type(), QgsStacObject::Type::Collection );
101111
QgsStacCollection *col = dynamic_cast< QgsStacCollection * >( obj );
@@ -105,7 +115,17 @@ void TestQgsStac::testParseLocalCollection()
105115
QCOMPARE( col->stacVersion(), QLatin1String( "1.0.0" ) );
106116
QCOMPARE( col->title(), QLatin1String( "Simple Example Collection" ) );
107117
QCOMPARE( col->description(), QLatin1String( "A simple collection demonstrating core catalog fields with links to a couple of items" ) );
108-
QCOMPARE( col->links().size(), 5 );
118+
119+
// check that relative links are correctly resolved into absolute links
120+
const QVector<QgsStacLink> links = col->links();
121+
QCOMPARE( links.size(), 5 );
122+
const QString basePath = url.adjusted( QUrl::RemoveFilename ).toString();
123+
QCOMPARE( links.at( 0 ).href(), QStringLiteral( "%1collection.json" ).arg( basePath ) );
124+
QCOMPARE( links.at( 1 ).href(), QStringLiteral( "%1simple-item.json" ).arg( basePath ) );
125+
QCOMPARE( links.at( 2 ).href(), QStringLiteral( "%1core-item.json" ).arg( basePath ) );
126+
QCOMPARE( links.at( 3 ).href(), QStringLiteral( "%1extended-item.json" ).arg( basePath ) );
127+
QCOMPARE( links.at( 4 ).href(), QStringLiteral( "https://raw.githubusercontent.com/radiantearth/stac-spec/v1.0.0/examples/collection.json" ) );
128+
109129
QCOMPARE( col->providers().size(), 1 );
110130
QCOMPARE( col->stacExtensions().size(), 3 );
111131
QCOMPARE( col->license(), QLatin1String( "CC-BY-4.0" ) );
@@ -133,19 +153,33 @@ void TestQgsStac::testParseLocalCollection()
133153

134154
void TestQgsStac::testParseLocalItem()
135155
{
136-
const QString fileName = mDataDir + QStringLiteral( "core-item.json" );
156+
const QUrl url( QStringLiteral( "file://%1%2" ).arg( mDataDir, QStringLiteral( "core-item.json" ) ) );
137157
QgsStacController c;
138-
QgsStacObject *obj = c.fetchStacObject( QStringLiteral( "file://%1" ).arg( fileName ) );
158+
QgsStacObject *obj = c.fetchStacObject( url.toString() );
139159
QVERIFY( obj );
140160
QCOMPARE( obj->type(), QgsStacObject::Type::Item );
141161
QgsStacItem *item = dynamic_cast<QgsStacItem *>( obj );
142162

143163
QVERIFY( item );
144164
QCOMPARE( item->id(), QLatin1String( "20201211_223832_CS2" ) );
145165
QCOMPARE( item->stacVersion(), QLatin1String( "1.0.0" ) );
146-
QCOMPARE( item->links().size(), 4 );
147166
QCOMPARE( item->stacExtensions().size(), 0 );
167+
168+
// check that relative links are correctly resolved into absolute links
169+
const QVector<QgsStacLink> links = item->links();
170+
QCOMPARE( links.size(), 4 );
171+
const QString basePath = url.adjusted( QUrl::RemoveFilename ).toString();
172+
QCOMPARE( links.at( 0 ).href(), QStringLiteral( "%1collection.json" ).arg( basePath ) );
173+
QCOMPARE( links.at( 1 ).href(), QStringLiteral( "%1collection.json" ).arg( basePath ) );
174+
QCOMPARE( links.at( 2 ).href(), QStringLiteral( "%1collection.json" ).arg( basePath ) );
175+
QCOMPARE( links.at( 3 ).href(), QStringLiteral( "http://remotedata.io/catalog/20201211_223832_CS2/index.html" ) );
176+
148177
QCOMPARE( item->assets().size(), 6 );
178+
QgsStacAsset asset = item->assets().value( QStringLiteral( "analytic" ), QgsStacAsset( {}, {}, {}, {}, {} ) );
179+
QCOMPARE( asset.href(), basePath + QStringLiteral( "20201211_223832_CS2_analytic.tif" ) );
180+
181+
asset = item->assets().value( QStringLiteral( "thumbnail" ), QgsStacAsset( {}, {}, {}, {}, {} ) );
182+
QCOMPARE( asset.href(), QStringLiteral( "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2.jpg" ) );
149183

150184
delete item;
151185
}

tests/testdata/stac/core-item.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@
8181
],
8282
"assets": {
8383
"analytic": {
84-
"href": "https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2_analytic.tif",
84+
"href": "./20201211_223832_CS2_analytic.tif",
8585
"type": "image/tiff; application=geotiff; profile=cloud-optimized",
8686
"title": "4-Band Analytic",
8787
"roles": [

0 commit comments

Comments
 (0)