From 0ca2d7ed365d89d6491e56656ef0d97824b97879 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 19 Jun 2024 14:21:44 +1000 Subject: [PATCH 01/19] [gdal] Ensure cloud vsi prefixes are correctly decoded --- src/core/providers/ogr/qgsogrprovider.cpp | 2 +- src/core/qgsgdalutils.cpp | 9 ++++----- tests/src/core/testqgsgdalprovider.cpp | 20 +++++++++++++++++++- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/core/providers/ogr/qgsogrprovider.cpp b/src/core/providers/ogr/qgsogrprovider.cpp index 2c78a773274e..31691e57b0f4 100644 --- a/src/core/providers/ogr/qgsogrprovider.cpp +++ b/src/core/providers/ogr/qgsogrprovider.cpp @@ -4193,7 +4193,7 @@ void QgsOgrProvider::open( OpenMode mode ) // Try to open using VSIFileHandler // see http://trac.osgeo.org/gdal/wiki/UserDocs/ReadInZip const QString vsiPrefix = QgsGdalUtils::vsiPrefixForPath( dataSourceUri( true ) ); - if ( !vsiPrefix.isEmpty() || mFilePath.startsWith( QLatin1String( "/vsicurl/" ) ) ) + if ( ( !vsiPrefix.isEmpty() && vsiPrefix != QStringLiteral( "/vsimem/" ) ) || mFilePath.startsWith( QLatin1String( "/vsicurl/" ) ) ) { // GDAL>=1.8.0 has write support for zip, but read and write operations // cannot be interleaved, so for now just use read-only. diff --git a/src/core/qgsgdalutils.cpp b/src/core/qgsgdalutils.cpp index 1252051bd6b3..dc43074f6607 100644 --- a/src/core/qgsgdalutils.cpp +++ b/src/core/qgsgdalutils.cpp @@ -711,11 +711,10 @@ QString QgsGdalUtils::vsiPrefixForPath( const QString &path ) { const QStringList vsiPrefixes = QgsGdalUtils::vsiArchivePrefixes(); - for ( const QString &vsiPrefix : vsiPrefixes ) - { - if ( path.startsWith( vsiPrefix, Qt::CaseInsensitive ) ) - return vsiPrefix; - } + const thread_local QRegularExpression vsiRx( QStringLiteral( "^(/vsi.+?/)" ), QRegularExpression::PatternOption::CaseInsensitiveOption ); + const QRegularExpressionMatch vsiMatch = vsiRx.match( path ); + if ( vsiMatch.hasMatch() ) + return vsiMatch.captured( 1 ); if ( path.endsWith( QLatin1String( ".shp.zip" ), Qt::CaseInsensitive ) ) { diff --git a/tests/src/core/testqgsgdalprovider.cpp b/tests/src/core/testqgsgdalprovider.cpp index 5a1a7e2b480b..0119c76745ed 100644 --- a/tests/src/core/testqgsgdalprovider.cpp +++ b/tests/src/core/testqgsgdalprovider.cpp @@ -138,8 +138,15 @@ void TestQgsGdalProvider::decodeUri() // test authcfg with vsicurl URI uri = QStringLiteral( "/vsicurl/https://www.qgis.org/dataset.tif authcfg='1234567'" ); components = QgsProviderRegistry::instance()->decodeUri( QStringLiteral( "gdal" ), uri ); - QCOMPARE( components.value( QStringLiteral( "path" ) ).toString(), QString( "/vsicurl/https://www.qgis.org/dataset.tif" ) ); + QCOMPARE( components.value( QStringLiteral( "path" ) ).toString(), QString( "https://www.qgis.org/dataset.tif" ) ); + QCOMPARE( components.value( QStringLiteral( "vsiPrefix" ) ).toString(), QString( "/vsicurl/" ) ); QCOMPARE( components.value( QStringLiteral( "authcfg" ) ).toString(), QString( "1234567" ) ); + + // vsis3 + uri = QStringLiteral( "/vsis3/nz-elevation/auckland/auckland-north_2016-2018/dem_1m/2193/AY30_10000_0405.tiff" ); + components = QgsProviderRegistry::instance()->decodeUri( QStringLiteral( "gdal" ), uri ); + QCOMPARE( components.value( QStringLiteral( "path" ) ).toString(), QString( "nz-elevation/auckland/auckland-north_2016-2018/dem_1m/2193/AY30_10000_0405.tiff" ) ); + QCOMPARE( components.value( QStringLiteral( "vsiPrefix" ) ).toString(), QString( "/vsis3/" ) ); } void TestQgsGdalProvider::encodeUri() @@ -162,6 +169,17 @@ void TestQgsGdalProvider::encodeUri() parts.insert( QStringLiteral( "path" ), QStringLiteral( "/vsicurl/https://www.qgis.org/dataset.tif" ) ); parts.insert( QStringLiteral( "authcfg" ), QStringLiteral( "1234567" ) ); QCOMPARE( QgsProviderRegistry::instance()->encodeUri( QStringLiteral( "gdal" ), parts ), QStringLiteral( "/vsicurl/https://www.qgis.org/dataset.tif authcfg='1234567'" ) ); + parts.clear(); + parts.insert( QStringLiteral( "path" ), QStringLiteral( "https://www.qgis.org/dataset.tif" ) ); + parts.insert( QStringLiteral( "vsiPrefix" ), QStringLiteral( "/vsicurl/" ) ); + parts.insert( QStringLiteral( "authcfg" ), QStringLiteral( "1234567" ) ); + QCOMPARE( QgsProviderRegistry::instance()->encodeUri( QStringLiteral( "gdal" ), parts ), QStringLiteral( "/vsicurl/https://www.qgis.org/dataset.tif authcfg='1234567'" ) ); + + // vsis3 + parts.clear(); + parts.insert( QStringLiteral( "vsiPrefix" ), QStringLiteral( "/vsis3/" ) ); + parts.insert( QStringLiteral( "path" ), QStringLiteral( "nz-elevation/auckland/auckland-north_2016-2018/dem_1m/2193/AY30_10000_0405.tiff" ) ); + QCOMPARE( QgsProviderRegistry::instance()->encodeUri( QStringLiteral( "gdal" ), parts ), QStringLiteral( "/vsis3/nz-elevation/auckland/auckland-north_2016-2018/dem_1m/2193/AY30_10000_0405.tiff" ) ); } void TestQgsGdalProvider::scaleDataType() From 219be786b6bb08afd0a97d55bd49c6b803714c5e Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 19 Jun 2024 14:27:05 +1000 Subject: [PATCH 02/19] [gdal/ogr] Allow storing credential key/value credential options in uris Extends decode/encodeUri to handle credential options. This is modeled off the existing support for storing open options. When credential options are found in a layer's URI, we use GDAL's VSISetPathSpecificOption to set the credential option for that VSI driver and bucket. This allows per-vsi driver & bucket credentials for GDAL/OGR layers, whereas other approaches (like environment variable setting) force a single set of credentials to be used for an entire QGIS session. Requires GDAL 3.5+ --- .../providers/gdal/qgsgdalproviderbase.cpp | 61 ++++++++++++++++++- src/core/providers/ogr/qgsogrprovider.cpp | 27 +++++++- .../providers/ogr/qgsogrprovidermetadata.cpp | 37 ++++++++++- .../providers/ogr/qgsogrproviderutils.cpp | 10 ++- src/core/providers/ogr/qgsogrproviderutils.h | 3 +- .../providers/ogr/qgsogrdbsourceselect.cpp | 4 +- tests/src/core/testqgsgdalprovider.cpp | 32 ++++++++++ tests/src/core/testqgsogrprovider.cpp | 56 ++++++++++++++++- tests/src/python/test_provider_gdal.py | 17 ++++++ tests/src/python/test_provider_ogr.py | 18 ++++++ 10 files changed, 252 insertions(+), 13 deletions(-) diff --git a/src/core/providers/gdal/qgsgdalproviderbase.cpp b/src/core/providers/gdal/qgsgdalproviderbase.cpp index 1a219a9f54b4..60aba5adf1dd 100644 --- a/src/core/providers/gdal/qgsgdalproviderbase.cpp +++ b/src/core/providers/gdal/qgsgdalproviderbase.cpp @@ -27,6 +27,7 @@ #include "qgsgdalproviderbase.h" #include "qgsgdalutils.h" #include "qgssettings.h" +#include "qgsmessagelog.h" #include #include @@ -290,6 +291,32 @@ GDALDatasetH QgsGdalProviderBase::gdalOpen( const QString &uri, unsigned int nOp option.toUtf8().constData() ); } + const QString vsiPrefix = parts.value( QStringLiteral( "vsiPrefix" ) ).toString(); + const QString vsiSuffix = parts.value( QStringLiteral( "vsiSuffix" ) ).toString(); + + const QVariantMap credentialOptions = parts.value( QStringLiteral( "credentialOptions" ) ).toMap(); + parts.remove( QStringLiteral( "credentialOptions" ) ); + if ( !credentialOptions.isEmpty() && !vsiPrefix.isEmpty() ) + { + const thread_local QRegularExpression bucketRx( QStringLiteral( "^(.*?)/" ) ); + const QRegularExpressionMatch bucketMatch = bucketRx.match( parts.value( QStringLiteral( "path" ) ).toString() ); + if ( bucketMatch.hasMatch() ) + { + const QString bucket = vsiPrefix + bucketMatch.captured( 1 ); + for ( auto it = credentialOptions.constBegin(); it != credentialOptions.constEnd(); ++it ) + { +#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3, 6, 0) + VSISetPathSpecificOption( bucket.toUtf8().constData(), it.key().toUtf8().constData(), it.value().toString().toUtf8().constData() ); +#elif GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3, 5, 0) + VSISetCredential( bucket.toUtf8().constData(), it.key().toUtf8().constData(), it.value().toString().toUtf8().constData() ); +#else + ( void )bucket; + QgsMessageLog::logMessage( QObject::tr( "Cannot use VSI credential options on GDAL versions earlier than 3.5" ), QStringLiteral( "GDAL" ), Qgis::MessageLevel::Critical ); +#endif + } + } + } + const bool modify_OGR_GPKG_FOREIGN_KEY_CHECK = !CPLGetConfigOption( "OGR_GPKG_FOREIGN_KEY_CHECK", nullptr ); if ( modify_OGR_GPKG_FOREIGN_KEY_CHECK ) { @@ -301,8 +328,6 @@ GDALDatasetH QgsGdalProviderBase::gdalOpen( const QString &uri, unsigned int nOp if ( !hDS ) { - const QString vsiPrefix = parts.value( QStringLiteral( "vsiPrefix" ) ).toString(); - const QString vsiSuffix = parts.value( QStringLiteral( "vsiSuffix" ) ).toString(); if ( vsiSuffix.isEmpty() && QgsGdalUtils::isVsiArchivePrefix( vsiPrefix ) ) { // in the case that a direct path to a vsi supported archive was specified BUT @@ -391,6 +416,7 @@ QVariantMap QgsGdalProviderBase::decodeGdalUri( const QString &uri ) QString layerName; QString authcfg; QStringList openOptions; + QVariantMap credentialOptions; const thread_local QRegularExpression authcfgRegex( " authcfg='([^']+)'" ); QRegularExpressionMatch match; @@ -451,6 +477,26 @@ QVariantMap QgsGdalProviderBase::decodeGdalUri( const QString &uri ) break; } } + + const thread_local QRegularExpression credentialOptionRegex( QStringLiteral( "\\|credential:([^|]*)" ) ); + const thread_local QRegularExpression credentialOptionKeyValueRegex( QStringLiteral( "(.*?)=(.*)" ) ); + while ( true ) + { + const QRegularExpressionMatch match = credentialOptionRegex.match( path ); + if ( match.hasMatch() ) + { + const QRegularExpressionMatch keyValueMatch = credentialOptionKeyValueRegex.match( match.captured( 1 ) ); + if ( keyValueMatch.hasMatch() ) + { + credentialOptions.insert( keyValueMatch.captured( 1 ), keyValueMatch.captured( 2 ) ); + } + path = path.remove( match.capturedStart( 0 ), match.capturedLength( 0 ) ); + } + else + { + break; + } + } } QVariantMap uriComponents; @@ -458,6 +504,8 @@ QVariantMap QgsGdalProviderBase::decodeGdalUri( const QString &uri ) uriComponents.insert( QStringLiteral( "layerName" ), layerName ); if ( !openOptions.isEmpty() ) uriComponents.insert( QStringLiteral( "openOptions" ), openOptions ); + if ( !credentialOptions.isEmpty() ) + uriComponents.insert( QStringLiteral( "credentialOptions" ), credentialOptions ); if ( !vsiPrefix.isEmpty() ) uriComponents.insert( QStringLiteral( "vsiPrefix" ), vsiPrefix ); if ( !vsiSuffix.isEmpty() ) @@ -494,6 +542,15 @@ QString QgsGdalProviderBase::encodeGdalUri( const QVariantMap &parts ) uri += openOption; } + const QVariantMap credentialOptions = parts.value( QStringLiteral( "credentialOptions" ) ).toMap(); + for ( auto it = credentialOptions.constBegin(); it != credentialOptions.constEnd(); ++it ) + { + if ( !it.value().toString().isEmpty() ) + { + uri += QStringLiteral( "|credential:%1=%2" ).arg( it.key(), it.value().toString() ); + } + } + if ( !authcfg.isEmpty() ) uri += QStringLiteral( " authcfg='%1'" ).arg( authcfg ); diff --git a/src/core/providers/ogr/qgsogrprovider.cpp b/src/core/providers/ogr/qgsogrprovider.cpp index 31691e57b0f4..0f2eea6896f1 100644 --- a/src/core/providers/ogr/qgsogrprovider.cpp +++ b/src/core/providers/ogr/qgsogrprovider.cpp @@ -418,14 +418,15 @@ QgsOgrProvider::QgsOgrProvider( QString const &uri, const ProviderOptions &optio QgsDebugMsgLevel( "Data source uri is [" + uri + ']', 2 ); + QVariantMap credentialOptions; mFilePath = QgsOgrProviderUtils::analyzeURI( uri, mIsSubLayer, mLayerIndex, mLayerName, mSubsetString, mOgrGeometryTypeFilter, - mOpenOptions ); - + mOpenOptions, + credentialOptions ); const QVariantMap parts = QgsOgrProviderMetadata().decodeUri( uri ); if ( parts.contains( QStringLiteral( "uniqueGeometryType" ) ) ) @@ -433,6 +434,28 @@ QgsOgrProvider::QgsOgrProvider( QString const &uri, const ProviderOptions &optio mUniqueGeometryType = parts.value( QStringLiteral( "uniqueGeometryType" ) ).toString() == QLatin1String( "yes" ); } + const QString vsiPrefix = parts.value( QStringLiteral( "vsiPrefix" ) ).toString(); + if ( !credentialOptions.isEmpty() && !vsiPrefix.isEmpty() ) + { + const thread_local QRegularExpression bucketRx( QStringLiteral( "^(.*?)/" ) ); + const QRegularExpressionMatch bucketMatch = bucketRx.match( parts.value( QStringLiteral( "path" ) ).toString() ); + if ( bucketMatch.hasMatch() ) + { + const QString bucket = vsiPrefix + bucketMatch.captured( 1 ); + for ( auto it = credentialOptions.constBegin(); it != credentialOptions.constEnd(); ++it ) + { +#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3, 6, 0) + VSISetPathSpecificOption( bucket.toUtf8().constData(), it.key().toUtf8().constData(), it.value().toString().toUtf8().constData() ); +#elif GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3, 5, 0) + VSISetCredential( bucket.toUtf8().constData(), it.key().toUtf8().constData(), it.value().toString().toUtf8().constData() ); +#else + ( void )bucket; + QgsMessageLog::logMessage( QObject::tr( "Cannot use VSI credential options on GDAL versions earlier than 3.5" ), QStringLiteral( "GDAL" ), Qgis::MessageLevel::Critical ); +#endif + } + } + } + // to be called only after mFilePath has been set invalidateNetworkCache(); diff --git a/src/core/providers/ogr/qgsogrprovidermetadata.cpp b/src/core/providers/ogr/qgsogrprovidermetadata.cpp index 145919b75ee0..29e155e2ecee 100644 --- a/src/core/providers/ogr/qgsogrprovidermetadata.cpp +++ b/src/core/providers/ogr/qgsogrprovidermetadata.cpp @@ -139,6 +139,7 @@ QVariantMap QgsOgrProviderMetadata::decodeUri( const QString &uri ) const QString geometryType; QString uniqueGeometryType; QStringList openOptions; + QVariantMap credentialOptions; QString databaseName; QString authcfg; @@ -179,6 +180,8 @@ QVariantMap QgsOgrProviderMetadata::decodeUri( const QString &uri ) const const thread_local QRegularExpression layerIdRegex( QStringLiteral( "\\|layerid=([^|]*)" ), QRegularExpression::PatternOption::CaseInsensitiveOption ); const thread_local QRegularExpression subsetRegex( QStringLiteral( "\\|subset=((?:.*[\r\n]*)*)\\Z" ) ); const thread_local QRegularExpression openOptionRegex( QStringLiteral( "\\|option:([^|]*)" ) ); + const thread_local QRegularExpression credentialOptionRegex( QStringLiteral( "\\|credential:([^|]*)" ) ); + const thread_local QRegularExpression credentialOptionKeyValueRegex( QStringLiteral( "(.*?)=(.*)" ) ); // we first try to split off the geometry type component, if that's present. That's a known quantity which // will never be more than a-z characters @@ -227,6 +230,24 @@ QVariantMap QgsOgrProviderMetadata::decodeUri( const QString &uri ) const } } + while ( true ) + { + const QRegularExpressionMatch match = credentialOptionRegex.match( path ); + if ( match.hasMatch() ) + { + const QRegularExpressionMatch keyValueMatch = credentialOptionKeyValueRegex.match( match.captured( 1 ) ); + if ( keyValueMatch.hasMatch() ) + { + credentialOptions.insert( keyValueMatch.captured( 1 ), keyValueMatch.captured( 2 ) ); + } + path = path.remove( match.capturedStart( 0 ), match.capturedLength( 0 ) ); + } + else + { + break; + } + } + // lastly, try to parse out the subset component. This is the biggest unknown, because it's // quite possible that a subset string will contain a | character. If we've already parsed // out all the other known |xxx=yyy tags, then we can safely assume that everything from "|subset=" to the @@ -275,6 +296,8 @@ QVariantMap QgsOgrProviderMetadata::decodeUri( const QString &uri ) const uriComponents.insert( QStringLiteral( "databaseName" ), databaseName ); if ( !openOptions.isEmpty() ) uriComponents.insert( QStringLiteral( "openOptions" ), openOptions ); + if ( !credentialOptions.isEmpty() ) + uriComponents.insert( QStringLiteral( "credentialOptions" ), credentialOptions ); if ( !vsiPrefix.isEmpty() ) uriComponents.insert( QStringLiteral( "vsiPrefix" ), vsiPrefix ); if ( !vsiSuffix.isEmpty() ) @@ -307,6 +330,16 @@ QString QgsOgrProviderMetadata::encodeUri( const QVariantMap &parts ) const uri += QLatin1String( "|option:" ); uri += openOption; } + + const QVariantMap credentialOptions = parts.value( QStringLiteral( "credentialOptions" ) ).toMap(); + for ( auto it = credentialOptions.constBegin(); it != credentialOptions.constEnd(); ++it ) + { + if ( !it.value().toString().isEmpty() ) + { + uri += QStringLiteral( "|credential:%1=%2" ).arg( it.key(), it.value().toString() ); + } + } + if ( !subset.isEmpty() ) uri += QStringLiteral( "|subset=%1" ).arg( subset ); if ( !authcfg.isEmpty() ) @@ -347,13 +380,15 @@ static QgsOgrLayerUniquePtr LoadDataSourceAndLayer( const QString &uri, bool upd QString subsetString; OGRwkbGeometryType ogrGeometryType; QStringList openOptions; + QVariantMap credentialOptions; filePath = QgsOgrProviderUtils::analyzeURI( uri, isSubLayer, layerIndex, layerName, subsetString, ogrGeometryType, - openOptions ); + openOptions, + credentialOptions ); if ( updateMode ) { diff --git a/src/core/providers/ogr/qgsogrproviderutils.cpp b/src/core/providers/ogr/qgsogrproviderutils.cpp index b3900b332c2a..ed29a42da452 100644 --- a/src/core/providers/ogr/qgsogrproviderutils.cpp +++ b/src/core/providers/ogr/qgsogrproviderutils.cpp @@ -73,7 +73,7 @@ QString QgsOgrProviderUtils::analyzeURI( QString const &uri, QString &layerName, QString &subsetString, OGRwkbGeometryType &ogrGeometryTypeFilter, - QStringList &openOptions ) + QStringList &openOptions, QVariantMap &credentialOptions ) { isSubLayer = false; layerIndex = 0; @@ -118,6 +118,8 @@ QString QgsOgrProviderUtils::analyzeURI( QString const &uri, openOptions = parts.value( QStringLiteral( "openOptions" ) ).toStringList(); } + credentialOptions = parts.value( QStringLiteral( "credentialOptions" ) ).toMap(); + const QString fullPath = parts.value( QStringLiteral( "vsiPrefix" ) ).toString() + parts.value( QStringLiteral( "path" ) ).toString() + parts.value( QStringLiteral( "vsiSuffix" ) ).toString(); @@ -1449,6 +1451,7 @@ static GDALDatasetH OpenHelper( const QString &dsName, papszOpenOptions = CSLAddString( papszOpenOptions, option.toUtf8().constData() ); } + GDALDatasetH hDS = QgsOgrProviderUtils::GDALOpenWrapper( QgsOgrProviderUtils::expandAuthConfig( dsName ).toUtf8().constData(), updateMode, papszOpenOptions, nullptr ); CSLDestroy( papszOpenOptions ); @@ -3461,14 +3464,15 @@ bool QgsOgrProviderUtils::deleteLayer( const QString &uri, QString &errCause ) QString subsetString; OGRwkbGeometryType ogrGeometryType; QStringList openOptions; + QVariantMap credentialOptions; QString filePath = analyzeURI( uri, isSubLayer, layerIndex, layerName, subsetString, ogrGeometryType, - openOptions ); - + openOptions, + credentialOptions ); gdal::dataset_unique_ptr hDS( GDALOpenEx( filePath.toUtf8().constData(), GDAL_OF_RASTER | GDAL_OF_VECTOR | GDAL_OF_UPDATE, nullptr, nullptr, nullptr ) ); if ( hDS && ( ! layerName.isEmpty() || layerIndex != -1 ) ) diff --git a/src/core/providers/ogr/qgsogrproviderutils.h b/src/core/providers/ogr/qgsogrproviderutils.h index cf43f55f301c..3826c1bb40d5 100644 --- a/src/core/providers/ogr/qgsogrproviderutils.h +++ b/src/core/providers/ogr/qgsogrproviderutils.h @@ -250,7 +250,8 @@ class CORE_EXPORT QgsOgrProviderUtils QString &layerName, QString &subsetString, OGRwkbGeometryType &ogrGeometryTypeFilter, - QStringList &openOptions ); + QStringList &openOptions, + QVariantMap &credentialOptions ); //! Whether a driver can share the same dataset handle among different layers static bool canDriverShareSameDatasetAmongLayers( const QString &driverName ); diff --git a/src/gui/providers/ogr/qgsogrdbsourceselect.cpp b/src/gui/providers/ogr/qgsogrdbsourceselect.cpp index 17a213ec110c..9fc37507ea07 100644 --- a/src/gui/providers/ogr/qgsogrdbsourceselect.cpp +++ b/src/gui/providers/ogr/qgsogrdbsourceselect.cpp @@ -385,13 +385,15 @@ bool QgsOgrDbSourceSelect::configureFromUri( const QString &uri ) QString subsetString; OGRwkbGeometryType ogrGeometryType; QStringList openOptions; + QVariantMap credentialOptions; const QString filePath = QgsOgrProviderUtils::analyzeURI( uri, isSubLayer, layerIndex, layerName, subsetString, ogrGeometryType, - openOptions ); + openOptions, + credentialOptions ); QFileInfo pathInfo { filePath }; const QString connectionName { pathInfo.fileName() }; diff --git a/tests/src/core/testqgsgdalprovider.cpp b/tests/src/core/testqgsgdalprovider.cpp index 0119c76745ed..8a446d6665b9 100644 --- a/tests/src/core/testqgsgdalprovider.cpp +++ b/tests/src/core/testqgsgdalprovider.cpp @@ -76,6 +76,7 @@ class TestQgsGdalProvider : public QgsTest void testGdalProviderQuerySublayersFastScan(); void testGdalProviderQuerySublayersFastScan_NetCDF(); void testGdalProviderAbsoluteRelativeUri(); + void testVsiCredentialOptions(); private: QString mTestDataDir; @@ -906,5 +907,36 @@ void TestQgsGdalProvider::testGdalProviderAbsoluteRelativeUri() QCOMPARE( mGdalMetadata->relativeToAbsoluteUri( relativeUri, context ), absoluteUri ); } +void TestQgsGdalProvider::testVsiCredentialOptions() +{ +#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3, 6, 0) + // test that credential options are correctly set when layer URI specifies them + std::unique_ptr< QgsRasterLayer > rl = std::make_unique< QgsRasterLayer >( QStringLiteral( "/vsis3/testbucket/test|credential:AWS_NO_SIGN_REQUEST=YES|credential:AWS_REGION=eu-central-1|credential:AWS_S3_ENDPOINT=localhost" ), QStringLiteral( "test" ), QStringLiteral( "gdal" ) ); + + // confirm that GDAL VSI configuration options are set + QString noSign( VSIGetPathSpecificOption( "/vsis3/testbucket", "AWS_NO_SIGN_REQUEST", nullptr ) ); + QCOMPARE( noSign, QStringLiteral( "YES" ) ); + QString region( VSIGetPathSpecificOption( "/vsis3/testbucket", "AWS_REGION", nullptr ) ); + QCOMPARE( region, QStringLiteral( "eu-central-1" ) ); + + // different bucket + noSign = QString( VSIGetPathSpecificOption( "/vsis3/another", "AWS_NO_SIGN_REQUEST", nullptr ) ); + QCOMPARE( noSign, QString() ); + region = QString( VSIGetPathSpecificOption( "/vsis3/another", "AWS_REGION", nullptr ) ); + QCOMPARE( region, QString() ); + + // credentials should be bucket specific + std::unique_ptr< QgsRasterLayer > rl2 = std::make_unique< QgsRasterLayer >( QStringLiteral( "/vsis3/another/test|credential:AWS_NO_SIGN_REQUEST=NO|credential:AWS_REGION=eu-central-2|credential:AWS_S3_ENDPOINT=localhost" ), QStringLiteral( "test" ), QStringLiteral( "gdal" ) ); + noSign = QString( VSIGetPathSpecificOption( "/vsis3/testbucket", "AWS_NO_SIGN_REQUEST", nullptr ) ); + QCOMPARE( noSign, QStringLiteral( "YES" ) ); + region = QString( VSIGetPathSpecificOption( "/vsis3/testbucket", "AWS_REGION", nullptr ) ); + QCOMPARE( region, QStringLiteral( "eu-central-1" ) ); + noSign = QString( VSIGetPathSpecificOption( "/vsis3/another", "AWS_NO_SIGN_REQUEST", nullptr ) ); + QCOMPARE( noSign, QStringLiteral( "NO" ) ); + region = QString( VSIGetPathSpecificOption( "/vsis3/another", "AWS_REGION", nullptr ) ); + QCOMPARE( region, QStringLiteral( "eu-central-2" ) ); +#endif +} + QGSTEST_MAIN( TestQgsGdalProvider ) #include "testqgsgdalprovider.moc" diff --git a/tests/src/core/testqgsogrprovider.cpp b/tests/src/core/testqgsogrprovider.cpp index 9caf7aef7c39..e70ff2637be2 100644 --- a/tests/src/core/testqgsogrprovider.cpp +++ b/tests/src/core/testqgsogrprovider.cpp @@ -30,7 +30,7 @@ #include #include - +#include /** * \ingroup UnitTests @@ -54,6 +54,7 @@ class TestQgsOgrProvider : public QgsTest void testCsvFeatureAddition(); void absoluteRelativeUri(); void testExtent(); + void testVsiCredentialOptions(); private: QString mTestDataDir; @@ -231,8 +232,14 @@ void TestQgsOgrProvider::decodeUri() // test authcfg with vsicurl URI parts = QgsProviderRegistry::instance()->decodeUri( QStringLiteral( "ogr" ), QStringLiteral( "/vsicurl/https://www.qgis.org/dataset.gpkg authcfg='1234567'" ) ); - QCOMPARE( parts.value( QStringLiteral( "path" ) ).toString(), QString( "/vsicurl/https://www.qgis.org/dataset.gpkg" ) ); + QCOMPARE( parts.value( QStringLiteral( "path" ) ).toString(), QString( "https://www.qgis.org/dataset.gpkg" ) ); + QCOMPARE( parts.value( QStringLiteral( "vsiPrefix" ) ).toString(), QString( "/vsicurl/" ) ); QCOMPARE( parts.value( QStringLiteral( "authcfg" ) ).toString(), QString( "1234567" ) ); + + // vsis3 + parts = QgsProviderRegistry::instance()->decodeUri( QStringLiteral( "ogr" ), QStringLiteral( "/vsis3/nz-elevation/auckland/auckland-north_2016-2018/auckland.shp" ) ); + QCOMPARE( parts.value( QStringLiteral( "path" ) ).toString(), QString( "nz-elevation/auckland/auckland-north_2016-2018/auckland.shp" ) ); + QCOMPARE( parts.value( QStringLiteral( "vsiPrefix" ) ).toString(), QString( "/vsis3/" ) ); } void TestQgsOgrProvider::encodeUri() @@ -270,6 +277,18 @@ void TestQgsOgrProvider::encodeUri() parts.insert( QStringLiteral( "path" ), QStringLiteral( "/vsicurl/https://www.qgis.org/dataset.gpkg" ) ); parts.insert( QStringLiteral( "authcfg" ), QStringLiteral( "1234567" ) ); QCOMPARE( QgsProviderRegistry::instance()->encodeUri( QStringLiteral( "ogr" ), parts ), QStringLiteral( "/vsicurl/https://www.qgis.org/dataset.gpkg authcfg='1234567'" ) ); + + parts.clear(); + parts.insert( QStringLiteral( "path" ), QStringLiteral( "https://www.qgis.org/dataset.gpkg" ) ); + parts.insert( QStringLiteral( "vsiPrefix" ), QStringLiteral( "/vsicurl/" ) ); + parts.insert( QStringLiteral( "authcfg" ), QStringLiteral( "1234567" ) ); + QCOMPARE( QgsProviderRegistry::instance()->encodeUri( QStringLiteral( "ogr" ), parts ), QStringLiteral( "/vsicurl/https://www.qgis.org/dataset.gpkg authcfg='1234567'" ) ); + + // vsis3 + parts.clear(); + parts.insert( QStringLiteral( "vsiPrefix" ), QStringLiteral( "/vsis3/" ) ); + parts.insert( QStringLiteral( "path" ), QStringLiteral( "nz-elevation/auckland/auckland-north_2016-2018/auckland.gpkg" ) ); + QCOMPARE( QgsProviderRegistry::instance()->encodeUri( QStringLiteral( "ogr" ), parts ), QStringLiteral( "/vsis3/nz-elevation/auckland/auckland-north_2016-2018/auckland.gpkg" ) ); } class ReadVectorLayer : public QThread @@ -324,7 +343,7 @@ void TestQgsOgrProvider::testThread() // Disabled by @m-kuhn // This test is flaky // See https://travis-ci.org/qgis/QGIS/jobs/505008602#L6464-L7108 - if ( !QgsTest::runFlakyTests() ) + if ( QgsTest::isCIRun() ) QSKIP( "This test is disabled on Travis CI environment" ); // After reading a QgsVectorLayer (getFeatures) from another thread the QgsOgrConnPoolGroup starts @@ -465,6 +484,37 @@ void TestQgsOgrProvider::testExtent() delete layer3D; } +void TestQgsOgrProvider::testVsiCredentialOptions() +{ +#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3, 6, 0) + // test that credential options are correctly set when layer URI specifies them + std::unique_ptr< QgsVectorLayer > vl = std::make_unique< QgsVectorLayer >( QStringLiteral( "/vsis3/testbucket/test|credential:AWS_NO_SIGN_REQUEST=YES|credential:AWS_REGION=eu-central-1|credential:AWS_S3_ENDPOINT=localhost" ), QStringLiteral( "test" ), QStringLiteral( "ogr" ) ); + + // confirm that GDAL VSI configuration options are set + QString noSign( VSIGetPathSpecificOption( "/vsis3/testbucket", "AWS_NO_SIGN_REQUEST", nullptr ) ); + QCOMPARE( noSign, QStringLiteral( "YES" ) ); + QString region( VSIGetPathSpecificOption( "/vsis3/testbucket", "AWS_REGION", nullptr ) ); + QCOMPARE( region, QStringLiteral( "eu-central-1" ) ); + + // different bucket + noSign = QString( VSIGetPathSpecificOption( "/vsis3/another", "AWS_NO_SIGN_REQUEST", nullptr ) ); + QCOMPARE( noSign, QString() ); + region = QString( VSIGetPathSpecificOption( "/vsis3/another", "AWS_REGION", nullptr ) ); + QCOMPARE( region, QString() ); + + // credentials should be bucket specific + std::unique_ptr< QgsVectorLayer > vl2 = std::make_unique< QgsVectorLayer >( QStringLiteral( "/vsis3/another/test|credential:AWS_NO_SIGN_REQUEST=NO|credential:AWS_REGION=eu-central-2|credential:AWS_S3_ENDPOINT=localhost" ), QStringLiteral( "test" ), QStringLiteral( "ogr" ) ); + noSign = QString( VSIGetPathSpecificOption( "/vsis3/testbucket", "AWS_NO_SIGN_REQUEST", nullptr ) ); + QCOMPARE( noSign, QStringLiteral( "YES" ) ); + region = QString( VSIGetPathSpecificOption( "/vsis3/testbucket", "AWS_REGION", nullptr ) ); + QCOMPARE( region, QStringLiteral( "eu-central-1" ) ); + noSign = QString( VSIGetPathSpecificOption( "/vsis3/another", "AWS_NO_SIGN_REQUEST", nullptr ) ); + QCOMPARE( noSign, QStringLiteral( "NO" ) ); + region = QString( VSIGetPathSpecificOption( "/vsis3/another", "AWS_REGION", nullptr ) ); + QCOMPARE( region, QStringLiteral( "eu-central-2" ) ); +#endif +} + QGSTEST_MAIN( TestQgsOgrProvider ) #include "testqgsogrprovider.moc" diff --git a/tests/src/python/test_provider_gdal.py b/tests/src/python/test_provider_gdal.py index 198ef01bd193..88c473437aad 100644 --- a/tests/src/python/test_provider_gdal.py +++ b/tests/src/python/test_provider_gdal.py @@ -113,6 +113,23 @@ def testDecodeEncodeUriOptions(self): encodedUri = QgsProviderRegistry.instance().encodeUri('gdal', parts) self.assertEqual(encodedUri, uri) + def testDecodeEncodeUriCredentialOptions(self): + """Test decodeUri/encodeUri credential options support""" + + uri = '/my/raster.pdf|option:AN=OPTION|credential:ANOTHER=BBB|credential:SOMEKEY=AAAAA' + parts = QgsProviderRegistry.instance().decodeUri('gdal', uri) + self.assertEqual(parts, { + 'path': '/my/raster.pdf', + 'layerName': None, + 'credentialOptions': { + 'ANOTHER': 'BBB', + 'SOMEKEY': 'AAAAA' + }, + 'openOptions': ['AN=OPTION'] + }) + encodedUri = QgsProviderRegistry.instance().encodeUri('gdal', parts) + self.assertEqual(encodedUri, uri) + def testDecodeEncodeUriVsizip(self): """Test decodeUri/encodeUri for /vsizip/ prefixed URIs""" diff --git a/tests/src/python/test_provider_ogr.py b/tests/src/python/test_provider_ogr.py index f176bf458ba6..acfda8867aa7 100644 --- a/tests/src/python/test_provider_ogr.py +++ b/tests/src/python/test_provider_ogr.py @@ -1626,6 +1626,24 @@ def testDecodeEncodeUriVsizip(self): encodedUri = QgsProviderRegistry.instance().encodeUri('ogr', parts) self.assertEqual(encodedUri, uri) + def testDecodeEncodeUriCredentialOptions(self): + """Test decodeUri/encodeUri credential options support""" + + uri = '/my/vector.shp|option:AN=OPTION|credential:ANOTHER=BBB|credential:SOMEKEY=AAAAA' + parts = QgsProviderRegistry.instance().decodeUri('ogr', uri) + self.assertEqual(parts, { + 'path': '/my/vector.shp', + 'layerId': None, + 'layerName': None, + 'credentialOptions': { + 'ANOTHER': 'BBB', + 'SOMEKEY': 'AAAAA' + }, + 'openOptions': ['AN=OPTION'] + }) + encodedUri = QgsProviderRegistry.instance().encodeUri('ogr', parts) + self.assertEqual(encodedUri, uri) + @unittest.skipIf(int(gdal.VersionInfo('VERSION_NUM')) < GDAL_COMPUTE_VERSION(3, 3, 0), "GDAL 3.3 required") def testFieldDomains(self): """ From ceea42d576f4fa6d2778e47c282b2b02c850deb9 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 19 Jun 2024 15:09:26 +1000 Subject: [PATCH 03/19] Make protocol handling in OGR/GDAL source select more robust - Don't rely on translated combo box strings, use item data instead - Reduce duplicate code - Add missing Hadoop option - Avoid unnecessary string list creation/splitting when populating combos --- src/gui/ogr/qgsogrhelperfunctions.cpp | 52 ++++++++----------- src/gui/ogr/qgsogrhelperfunctions.h | 5 ++ .../providers/gdal/qgsgdalsourceselect.cpp | 34 ++++++------ src/gui/providers/gdal/qgsgdalsourceselect.h | 3 -- src/gui/providers/ogr/qgsogrsourceselect.cpp | 36 ++++++------- src/gui/providers/ogr/qgsogrsourceselect.h | 2 - 6 files changed, 58 insertions(+), 74 deletions(-) diff --git a/src/gui/ogr/qgsogrhelperfunctions.cpp b/src/gui/ogr/qgsogrhelperfunctions.cpp index c3a0a703ec45..312c9a3a85be 100644 --- a/src/gui/ogr/qgsogrhelperfunctions.cpp +++ b/src/gui/ogr/qgsogrhelperfunctions.cpp @@ -221,7 +221,7 @@ QString createDatabaseURI( const QString &connectionType, const QString &host, c QString createProtocolURI( const QString &type, const QString &url, const QString &configId, const QString &username, const QString &password, bool expandAuthConfig ) { QString uri; - if ( type == QLatin1String( "HTTP/HTTPS/FTP" ) ) + if ( type == QLatin1String( "vsicurl" ) ) { uri = url; // If no protocol is provided in the URL, default to HTTP @@ -231,35 +231,17 @@ QString createProtocolURI( const QString &type, const QString &url, const QStrin } uri.prepend( QStringLiteral( "/vsicurl/" ) ); } - else if ( type == QLatin1String( "AWS S3" ) ) + else if ( type == QLatin1String( "vsis3" ) + || type == QLatin1String( "vsigs" ) + || type == QLatin1String( "vsiaz" ) + || type == QLatin1String( "vsiadls" ) + || type == QLatin1String( "vsioss" ) + || type == QLatin1String( "vsiswift" ) + || type == QLatin1String( "vsihdfs" ) + ) { uri = url; - uri.prepend( QStringLiteral( "/vsis3/" ) ); - } - else if ( type == QLatin1String( "Google Cloud Storage" ) ) - { - uri = url; - uri.prepend( QStringLiteral( "/vsigs/" ) ); - } - else if ( type == QLatin1String( "Microsoft Azure Blob" ) ) - { - uri = url; - uri.prepend( QStringLiteral( "/vsiaz/" ) ); - } - else if ( type == QLatin1String( "Microsoft Azure Data Lake Storage" ) ) - { - uri = url; - uri.prepend( QStringLiteral( "/vsiadls/" ) ); - } - else if ( type == QLatin1String( "Alibaba Cloud OSS" ) ) - { - uri = url; - uri.prepend( QStringLiteral( "/vsioss/" ) ); - } - else if ( type == QLatin1String( "OpenStack Swift Object Storage" ) ) - { - uri = url; - uri.prepend( QStringLiteral( "/vsiswift/" ) ); + uri.prepend( QStringLiteral( "/%1/" ).arg( type ) ); } // catching both GeoJSON and GeoJSONSeq else if ( type.startsWith( QLatin1String( "GeoJSON" ) ) ) @@ -274,8 +256,7 @@ QString createProtocolURI( const QString &type, const QString &url, const QStrin { uri = QStringLiteral( "DODS:%1" ).arg( url ); } - // Check beginning because of "experimental" - else if ( type.startsWith( QLatin1String( "WFS3" ) ) ) + else if ( type == QLatin1String( "WFS3" ) ) { uri = QStringLiteral( "WFS3:%1" ).arg( url ); } @@ -303,3 +284,14 @@ QString createProtocolURI( const QString &type, const QString &url, const QStrin } return uri; } + +bool isProtocolCloudType( const QString &protocol ) +{ + return ( protocol == QLatin1String( "vsis3" ) || + protocol == QLatin1String( "vsigs" ) || + protocol == QLatin1String( "vsiaz" ) || + protocol == QLatin1String( "vsiadls" ) || + protocol == QLatin1String( "vsioss" ) || + protocol == QLatin1String( "vsiswift" ) || + protocol == QLatin1String( "vsihdfs" ) ); +} diff --git a/src/gui/ogr/qgsogrhelperfunctions.h b/src/gui/ogr/qgsogrhelperfunctions.h index 5c58bf6d0b93..02d5ca68b00b 100644 --- a/src/gui/ogr/qgsogrhelperfunctions.h +++ b/src/gui/ogr/qgsogrhelperfunctions.h @@ -34,3 +34,8 @@ QString GUI_EXPORT createDatabaseURI( const QString &connectionType, const QStri * \note not available in python bindings */ QString GUI_EXPORT createProtocolURI( const QString &type, const QString &url, const QString &configId, const QString &username, const QString &password, bool expandAuthConfig = false ); + +/** + * Returns TRUE if \a protocol (eg "vsis3") is considered a cloud type. + */ +bool GUI_EXPORT isProtocolCloudType( const QString &protocol ); diff --git a/src/gui/providers/gdal/qgsgdalsourceselect.cpp b/src/gui/providers/gdal/qgsgdalsourceselect.cpp index fbbd43e82463..c3e9bd0a20cc 100644 --- a/src/gui/providers/gdal/qgsgdalsourceselect.cpp +++ b/src/gui/providers/gdal/qgsgdalsourceselect.cpp @@ -45,13 +45,19 @@ QgsGdalSourceSelect::QgsGdalSourceSelect( QWidget *parent, Qt::WindowFlags fl, Q whileBlocking( radioSrcFile )->setChecked( true ); protocolGroupBox->hide(); - QStringList protocolTypes = QStringLiteral( "HTTP/HTTPS/FTP,vsicurl;AWS S3,vsis3;Google Cloud Storage,vsigs" ).split( ';' ); - protocolTypes += QStringLiteral( "Microsoft Azure Blob,vsiaz;Microsoft Azure Data Lake Storage,vsiadls;Alibaba Cloud OSS,vsioss;OpenStack Swift Object Storage,vsiswift" ).split( ';' ); - for ( int i = 0; i < protocolTypes.count(); i++ ) + for ( const auto &protocol : + { + std::make_pair( QStringLiteral( "HTTP/HTTPS/FTP" ), QStringLiteral( "vsicurl" ) ), + std::make_pair( QStringLiteral( "AWS S3" ), QStringLiteral( "vsis3" ) ), + std::make_pair( QObject::tr( "Google Cloud Storage" ), QStringLiteral( "vsigs" ) ), + std::make_pair( QObject::tr( "Microsoft Azure Blob" ), QStringLiteral( "vsiaz" ) ), + std::make_pair( QObject::tr( "Microsoft Azure Data Lake Storage" ), QStringLiteral( "vsiadls" ) ), + std::make_pair( QObject::tr( "Alibaba Cloud OSS" ), QStringLiteral( "vsioss" ) ), + std::make_pair( QObject::tr( "OpenStack Swift Object Storage" ), QStringLiteral( "vsiswift" ) ), + std::make_pair( QObject::tr( "Hadoop File System" ), QStringLiteral( "vsihdfs" ) ), + } ) { - QString protocol = protocolTypes.at( i ); - if ( ( !protocol.isEmpty() ) && ( !protocol.isNull() ) ) - cmbProtocolTypes->addItem( protocol.split( ',' ).at( 0 ) ); + cmbProtocolTypes->addItem( protocol.first, protocol.second ); } mAuthWarning->setText( tr( " Additional credential options are required as documented here." ).arg( QLatin1String( "https://gdal.org/user/virtual_file_systems.html#drivers-supporting-virtual-file-systems" ) ) ); @@ -94,19 +100,9 @@ QgsGdalSourceSelect::QgsGdalSourceSelect( QWidget *parent, Qt::WindowFlags fl, Q mAuthSettingsProtocol->setDataprovider( QStringLiteral( "gdal" ) ); } -bool QgsGdalSourceSelect::isProtocolCloudType() -{ - return ( cmbProtocolTypes->currentText() == QLatin1String( "AWS S3" ) || - cmbProtocolTypes->currentText() == QLatin1String( "Google Cloud Storage" ) || - cmbProtocolTypes->currentText() == QLatin1String( "Microsoft Azure Blob" ) || - cmbProtocolTypes->currentText() == QLatin1String( "Microsoft Azure Data Lake Storage" ) || - cmbProtocolTypes->currentText() == QLatin1String( "Alibaba Cloud OSS" ) || - cmbProtocolTypes->currentText() == QLatin1String( "OpenStack Swift Object Storage" ) ); -} - void QgsGdalSourceSelect::setProtocolWidgetsVisibility() { - if ( isProtocolCloudType() ) + if ( isProtocolCloudType( cmbProtocolTypes->currentData().toString() ) ) { labelProtocolURI->hide(); protocolURI->hide(); @@ -323,7 +319,7 @@ void QgsGdalSourceSelect::computeDataSources() } else if ( radioSrcProtocol->isChecked() ) { - bool cloudType = isProtocolCloudType(); + bool cloudType = isProtocolCloudType( cmbProtocolTypes->currentData().toString() ); if ( !cloudType && protocolURI->text().isEmpty() ) { return; @@ -347,7 +343,7 @@ void QgsGdalSourceSelect::computeDataSources() if ( !openOptions.isEmpty() ) parts.insert( QStringLiteral( "openOptions" ), openOptions ); parts.insert( QStringLiteral( "path" ), - createProtocolURI( cmbProtocolTypes->currentText(), + createProtocolURI( cmbProtocolTypes->currentData().toString(), uri, mAuthSettingsProtocol->configId(), mAuthSettingsProtocol->username(), diff --git a/src/gui/providers/gdal/qgsgdalsourceselect.h b/src/gui/providers/gdal/qgsgdalsourceselect.h index a492882f45fa..e90dd6ba3ded 100644 --- a/src/gui/providers/gdal/qgsgdalsourceselect.h +++ b/src/gui/providers/gdal/qgsgdalsourceselect.h @@ -37,9 +37,6 @@ class QgsGdalSourceSelect : public QgsAbstractDataSourceWidget, private Ui::QgsG //! Constructor QgsGdalSourceSelect( QWidget *parent = nullptr, Qt::WindowFlags fl = QgsGuiUtils::ModalDialogFlags, QgsProviderRegistry::WidgetMode widgetMode = QgsProviderRegistry::WidgetMode::None ); - //! Returns whether the protocol is a cloud type - bool isProtocolCloudType(); - public slots: //! Determines the tables the user selected and closes the dialog void addButtonClicked() override; diff --git a/src/gui/providers/ogr/qgsogrsourceselect.cpp b/src/gui/providers/ogr/qgsogrsourceselect.cpp index 16d5118e897b..66edfb442272 100644 --- a/src/gui/providers/ogr/qgsogrsourceselect.cpp +++ b/src/gui/providers/ogr/qgsogrsourceselect.cpp @@ -95,14 +95,20 @@ QgsOgrSourceSelect::QgsOgrSourceSelect( QWidget *parent, Qt::WindowFlags fl, Qgs } //add protocol drivers - QStringList protocolTypes = QStringLiteral( "HTTP/HTTPS/FTP,vsicurl;AWS S3,vsis3;Google Cloud Storage,vsigs;" ).split( ';' ); - protocolTypes += QStringLiteral( "Microsoft Azure Blob,vsiaz;Microsoft Azure Data Lake Storage,vsiadls;Alibaba Cloud OSS,vsioss;OpenStack Swift Object Storage,vsiswift;WFS3 (experimental),WFS3" ).split( ';' ); - protocolTypes += QgsProviderRegistry::instance()->protocolDrivers().split( ';' ); - for ( int i = 0; i < protocolTypes.count(); i++ ) + for ( const auto &protocol : + { + std::make_pair( QStringLiteral( "HTTP/HTTPS/FTP" ), QStringLiteral( "vsicurl" ) ), + std::make_pair( QStringLiteral( "AWS S3" ), QStringLiteral( "vsis3" ) ), + std::make_pair( QObject::tr( "Google Cloud Storage" ), QStringLiteral( "vsigs" ) ), + std::make_pair( QObject::tr( "Microsoft Azure Blob" ), QStringLiteral( "vsiaz" ) ), + std::make_pair( QObject::tr( "Microsoft Azure Data Lake Storage" ), QStringLiteral( "vsiadls" ) ), + std::make_pair( QObject::tr( "Alibaba Cloud OSS" ), QStringLiteral( "vsioss" ) ), + std::make_pair( QObject::tr( "OpenStack Swift Object Storage" ), QStringLiteral( "vsiswift" ) ), + std::make_pair( QObject::tr( "Hadoop File System" ), QStringLiteral( "vsihdfs" ) ), + std::make_pair( QObject::tr( "WFS3 (Experimental)" ), QStringLiteral( "WFS3" ) ), + } ) { - QString protocolType = protocolTypes.at( i ); - if ( !protocolType.isEmpty() ) - cmbProtocolTypes->addItem( protocolType.split( ',' ).at( 0 ) ); + cmbProtocolTypes->addItem( protocol.first, protocol.second ); } cmbDatabaseTypes->blockSignals( false ); cmbConnections->blockSignals( false ); @@ -162,16 +168,6 @@ QString QgsOgrSourceSelect::dataSourceType() return mDataSourceType; } -bool QgsOgrSourceSelect::isProtocolCloudType() -{ - return ( cmbProtocolTypes->currentText() == QLatin1String( "AWS S3" ) || - cmbProtocolTypes->currentText() == QLatin1String( "Google Cloud Storage" ) || - cmbProtocolTypes->currentText() == QLatin1String( "Microsoft Azure Blob" ) || - cmbProtocolTypes->currentText() == QLatin1String( "Microsoft Azure Data Lake Storage" ) || - cmbProtocolTypes->currentText() == QLatin1String( "Alibaba Cloud OSS" ) || - cmbProtocolTypes->currentText() == QLatin1String( "OpenStack Swift Object Storage" ) ); -} - void QgsOgrSourceSelect::addNewConnection() { QgsNewOgrConnection *nc = new QgsNewOgrConnection( this, cmbDatabaseTypes->currentText() ); @@ -295,7 +291,7 @@ void QgsOgrSourceSelect::setSelectedConnection() void QgsOgrSourceSelect::setProtocolWidgetsVisibility() { - if ( isProtocolCloudType() ) + if ( isProtocolCloudType( cmbProtocolTypes->currentData().toString() ) ) { labelProtocolURI->hide(); protocolURI->hide(); @@ -401,7 +397,7 @@ void QgsOgrSourceSelect::computeDataSources( bool interactive ) } else if ( radioSrcProtocol->isChecked() ) { - bool cloudType = isProtocolCloudType(); + bool cloudType = isProtocolCloudType( cmbProtocolTypes->currentData().toString() ); if ( !cloudType && protocolURI->text().isEmpty() ) { if ( interactive ) @@ -437,7 +433,7 @@ void QgsOgrSourceSelect::computeDataSources( bool interactive ) if ( !openOptions.isEmpty() ) parts.insert( QStringLiteral( "openOptions" ), openOptions ); parts.insert( QStringLiteral( "path" ), - createProtocolURI( cmbProtocolTypes->currentText(), + createProtocolURI( cmbProtocolTypes->currentData().toString(), uri, mAuthSettingsProtocol->configId(), mAuthSettingsProtocol->username(), diff --git a/src/gui/providers/ogr/qgsogrsourceselect.h b/src/gui/providers/ogr/qgsogrsourceselect.h index 0e7dd1db7532..ed3f5722861e 100644 --- a/src/gui/providers/ogr/qgsogrsourceselect.h +++ b/src/gui/providers/ogr/qgsogrsourceselect.h @@ -61,8 +61,6 @@ class QgsOgrSourceSelect : public QgsAbstractDataSourceWidget, private Ui::QgsOg QString encoding(); //! Returns the connection type QString dataSourceType(); - //! Returns whether the protocol is a cloud type - bool isProtocolCloudType(); private: //! Stores the file vector filters From 781d568533af9fbeaa0370de9fad18e9cde2d407 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 20 Jun 2024 14:12:55 +1000 Subject: [PATCH 04/19] Create QgsGdalCredentialOptionsWidget A resuable widget for configuration GDAL credential options for vsi file systems --- .../qgsgdalcredentialoptionswidget.sip.in | 67 ++ python/PyQt6/gui/gui_auto.sip | 1 + .../qgsgdalcredentialoptionswidget.sip.in | 67 ++ python/gui/gui_auto.sip | 1 + src/gui/CMakeLists.txt | 2 + .../gdal/qgsgdalcredentialoptionswidget.cpp | 684 ++++++++++++++++++ .../gdal/qgsgdalcredentialoptionswidget.h | 180 +++++ src/ui/qgsgdalcredentialoptionswidgetbase.ui | 50 ++ 8 files changed, 1052 insertions(+) create mode 100644 python/PyQt6/gui/auto_generated/providers/gdal/qgsgdalcredentialoptionswidget.sip.in create mode 100644 python/gui/auto_generated/providers/gdal/qgsgdalcredentialoptionswidget.sip.in create mode 100644 src/gui/providers/gdal/qgsgdalcredentialoptionswidget.cpp create mode 100644 src/gui/providers/gdal/qgsgdalcredentialoptionswidget.h create mode 100644 src/ui/qgsgdalcredentialoptionswidgetbase.ui diff --git a/python/PyQt6/gui/auto_generated/providers/gdal/qgsgdalcredentialoptionswidget.sip.in b/python/PyQt6/gui/auto_generated/providers/gdal/qgsgdalcredentialoptionswidget.sip.in new file mode 100644 index 000000000000..fdf513612462 --- /dev/null +++ b/python/PyQt6/gui/auto_generated/providers/gdal/qgsgdalcredentialoptionswidget.sip.in @@ -0,0 +1,67 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/providers/gdal/qgsgdalcredentialoptionswidget.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + +class QgsGdalCredentialOptionsWidget : QWidget +{ +%Docstring(signature="appended") +A widget for configuring GDAL credential options. + +.. versionadded:: 3.40 +%End + +%TypeHeaderCode +#include "qgsgdalcredentialoptionswidget.h" +%End + public: + + QgsGdalCredentialOptionsWidget( QWidget *parent = 0 ); +%Docstring +Constructor for QgsGdalCredentialOptionsWidget, with the specified ``parent`` widget. +%End + + void setDriver( const QString &driver ); +%Docstring +Sets the corresponding ``driver``. + +This should match a GDAL VSI driver, eg "vsis3". +%End + + QVariantMap credentialOptions() const; +%Docstring +Returns the credential options configured in the widget. + +.. seealso:: :py:func:`setCredentialOptions` +%End + + void setCredentialOptions( const QVariantMap &options ); +%Docstring +Sets the credential ``options`` to show in the widget. + +.. seealso:: :py:func:`credentialOptions` +%End + + signals: + + void optionsChanged(); +%Docstring +Emitted when the credential options are changed in the widget. +%End + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/providers/gdal/qgsgdalcredentialoptionswidget.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/PyQt6/gui/gui_auto.sip b/python/PyQt6/gui/gui_auto.sip index 0d19ffb9ef17..46ca4ea13219 100644 --- a/python/PyQt6/gui/gui_auto.sip +++ b/python/PyQt6/gui/gui_auto.sip @@ -450,6 +450,7 @@ %Include auto_generated/proj/qgsprojectionselectionwidget.sip %Include auto_generated/proj/qgsrecentcoordinatereferencesystemsmodel.sip %Include auto_generated/providers/qgsabstractdbsourceselect.sip +%Include auto_generated/providers/gdal/qgsgdalcredentialoptionswidget.sip %Include auto_generated/raster/qgsrasterattributetablewidget.sip %Include auto_generated/raster/qgsrasterattributetabledialog.sip %Include auto_generated/raster/qgscolorrampshaderwidget.sip diff --git a/python/gui/auto_generated/providers/gdal/qgsgdalcredentialoptionswidget.sip.in b/python/gui/auto_generated/providers/gdal/qgsgdalcredentialoptionswidget.sip.in new file mode 100644 index 000000000000..fdf513612462 --- /dev/null +++ b/python/gui/auto_generated/providers/gdal/qgsgdalcredentialoptionswidget.sip.in @@ -0,0 +1,67 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/providers/gdal/qgsgdalcredentialoptionswidget.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + +class QgsGdalCredentialOptionsWidget : QWidget +{ +%Docstring(signature="appended") +A widget for configuring GDAL credential options. + +.. versionadded:: 3.40 +%End + +%TypeHeaderCode +#include "qgsgdalcredentialoptionswidget.h" +%End + public: + + QgsGdalCredentialOptionsWidget( QWidget *parent = 0 ); +%Docstring +Constructor for QgsGdalCredentialOptionsWidget, with the specified ``parent`` widget. +%End + + void setDriver( const QString &driver ); +%Docstring +Sets the corresponding ``driver``. + +This should match a GDAL VSI driver, eg "vsis3". +%End + + QVariantMap credentialOptions() const; +%Docstring +Returns the credential options configured in the widget. + +.. seealso:: :py:func:`setCredentialOptions` +%End + + void setCredentialOptions( const QVariantMap &options ); +%Docstring +Sets the credential ``options`` to show in the widget. + +.. seealso:: :py:func:`credentialOptions` +%End + + signals: + + void optionsChanged(); +%Docstring +Emitted when the credential options are changed in the widget. +%End + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/providers/gdal/qgsgdalcredentialoptionswidget.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/gui/gui_auto.sip b/python/gui/gui_auto.sip index 0d19ffb9ef17..46ca4ea13219 100644 --- a/python/gui/gui_auto.sip +++ b/python/gui/gui_auto.sip @@ -450,6 +450,7 @@ %Include auto_generated/proj/qgsprojectionselectionwidget.sip %Include auto_generated/proj/qgsrecentcoordinatereferencesystemsmodel.sip %Include auto_generated/providers/qgsabstractdbsourceselect.sip +%Include auto_generated/providers/gdal/qgsgdalcredentialoptionswidget.sip %Include auto_generated/raster/qgsrasterattributetablewidget.sip %Include auto_generated/raster/qgsrasterattributetabledialog.sip %Include auto_generated/raster/qgscolorrampshaderwidget.sip diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 1f74b0484200..2e54f7a6d60d 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -443,6 +443,7 @@ set(QGIS_GUI_SRCS providers/qgspointcloudproviderguimetadata.cpp providers/qgspointcloudsourceselect.cpp + providers/gdal/qgsgdalcredentialoptionswidget.cpp providers/gdal/qgsgdalfilesourcewidget.cpp providers/gdal/qgsgdalsourceselect.cpp providers/gdal/qgsgdalguiprovider.cpp @@ -1414,6 +1415,7 @@ set(QGIS_GUI_HDRS providers/qgspointcloudsourceselect.h providers/qgspointcloudproviderguimetadata.h + providers/gdal/qgsgdalcredentialoptionswidget.h providers/gdal/qgsgdalfilesourcewidget.h providers/gdal/qgsgdalguiprovider.h providers/gdal/qgsgdalsourceselect.h diff --git a/src/gui/providers/gdal/qgsgdalcredentialoptionswidget.cpp b/src/gui/providers/gdal/qgsgdalcredentialoptionswidget.cpp new file mode 100644 index 000000000000..46eb85566f49 --- /dev/null +++ b/src/gui/providers/gdal/qgsgdalcredentialoptionswidget.cpp @@ -0,0 +1,684 @@ +/*************************************************************************** + qgsgdalcredentialoptionswidget.h + ------------------- + begin : June 2024 + copyright : (C) 2024 by Nyall Dawson + email : nyall dot dawson at gmail dot com + + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsgdalcredentialoptionswidget.h" +#include "ogr/qgsogrhelperfunctions.h" +#include "qgsvariantutils.h" +#include "qgsapplication.h" + +#include +#include +#include +#include +#include + + +// +// QgsGdalCredentialOptionsModel +// + +QgsGdalCredentialOptionsModel::QgsGdalCredentialOptionsModel( QObject *parent ) + : QAbstractItemModel( parent ) +{ + +} + +int QgsGdalCredentialOptionsModel::columnCount( const QModelIndex & ) const +{ + return 3; +} + +int QgsGdalCredentialOptionsModel::rowCount( const QModelIndex &parent ) const +{ + if ( parent.isValid() ) + return 0; + return mCredentialOptions.size(); +} + +QModelIndex QgsGdalCredentialOptionsModel::index( int row, int column, const QModelIndex &parent ) const +{ + if ( hasIndex( row, column, parent ) ) + { + return createIndex( row, column, row ); + } + + return QModelIndex(); +} + +QModelIndex QgsGdalCredentialOptionsModel::parent( const QModelIndex &child ) const +{ + Q_UNUSED( child ) + return QModelIndex(); +} + +Qt::ItemFlags QgsGdalCredentialOptionsModel::flags( const QModelIndex &index ) const +{ + if ( !index.isValid() ) + return Qt::ItemFlags(); + + if ( index.row() < 0 || index.row() >= mCredentialOptions.size() || index.column() < 0 || index.column() >= columnCount() ) + return Qt::ItemFlags(); + + switch ( index.column() ) + { + case Column::Key: + case Column::Value: + return Qt::ItemFlag::ItemIsEnabled | Qt::ItemFlag::ItemIsEditable | Qt::ItemFlag::ItemIsSelectable; + case Column::Actions: + return Qt::ItemFlag::ItemIsEnabled; + default: + break; + } + + return Qt::ItemFlags(); +} + +QVariant QgsGdalCredentialOptionsModel::data( const QModelIndex &index, int role ) const +{ + if ( !index.isValid() ) + return QVariant(); + + if ( index.row() < 0 || index.row() >= mCredentialOptions.size() || index.column() < 0 || index.column() >= columnCount() ) + return QVariant(); + + const QPair< QString, QString > option = mCredentialOptions.at( index.row() ); + const GdalOption gdalOption = QgsGdalCredentialOptionsModel::option( option.first ); + + switch ( role ) + { + case Qt::DisplayRole: + { + switch ( index.column() ) + { + case Column::Key: + return option.first; + + case Column::Value: + return gdalOption.type == GdalOption::Type::Boolean ? ( option.second == QStringLiteral( "YES" ) ? tr( "Yes" ) : option.second == QStringLiteral( "NO" ) ? tr( "No" ) : option.second ) + : option.second; + + default: + break; + } + break; + } + + case Qt::EditRole: + { + switch ( index.column() ) + { + case Column::Key: + return option.first; + + case Column::Value: + return option.second; + + default: + break; + } + break; + } + + case Qt::ToolTipRole: + { + switch ( index.column() ) + { + case Column::Key: + case Column::Value: + return mDescriptions.value( option.first, option.first ); + + case Column::Actions: + return tr( "Remove option" ); + + default: + break; + } + break; + } + + default: + break; + } + return QVariant(); +} + +bool QgsGdalCredentialOptionsModel::setData( const QModelIndex &index, const QVariant &value, int role ) +{ + if ( !index.isValid() ) + return false; + + if ( index.row() > mCredentialOptions.size() || index.row() < 0 ) + return false; + + QPair< QString, QString > &option = mCredentialOptions[index.row()]; + + switch ( role ) + { + case Qt::EditRole: + { + switch ( index.column() ) + { + case Column::Key: + { + const bool wasInvalid = option.first.isEmpty(); + if ( value.toString().isEmpty() ) + { + if ( wasInvalid ) + break; + } + else + { + if ( option.first != value.toString() ) + option.second = QgsGdalCredentialOptionsModel::option( value.toString() ).defaultValue; + + option.first = value.toString(); + if ( wasInvalid ) + { + emit dataChanged( createIndex( index.row(), 0 ), createIndex( index.row(), columnCount() ) ); + } + } + emit dataChanged( createIndex( index.row(), 0 ), createIndex( index.row(), columnCount() ) ); + if ( wasInvalid ) + { + beginInsertRows( QModelIndex(), mCredentialOptions.size(), mCredentialOptions.size() ); + mCredentialOptions.append( qMakePair( QString(), QString() ) ); + endInsertRows(); + } + emit optionsChanged(); + break; + } + + case Column::Value: + { + option.second = value.toString(); + emit dataChanged( index, index, QVector() << role ); + emit optionsChanged(); + break; + } + + default: + break; + } + return true; + } + + default: + break; + } + + return false; +} + +bool QgsGdalCredentialOptionsModel::insertRows( int position, int rows, const QModelIndex &parent ) +{ + Q_UNUSED( parent ) + beginInsertRows( QModelIndex(), position, position + rows - 1 ); + for ( int i = 0; i < rows; ++i ) + { + mCredentialOptions.insert( position, qMakePair( QString(), QString() ) ); + } + endInsertRows(); + emit optionsChanged(); + return true; +} + +bool QgsGdalCredentialOptionsModel::removeRows( int position, int rows, const QModelIndex &parent ) +{ + Q_UNUSED( parent ) + beginRemoveRows( QModelIndex(), position, position + rows - 1 ); + for ( int i = 0; i < rows; ++i ) + mCredentialOptions.removeAt( position ); + endRemoveRows(); + + if ( mCredentialOptions.empty() || !mCredentialOptions.last().first.isEmpty() ) + { + beginInsertRows( QModelIndex(), mCredentialOptions.size(), mCredentialOptions.size() ); + mCredentialOptions.append( qMakePair( QString(), QString() ) ); + endInsertRows(); + } + emit optionsChanged(); + return true; +} + +void QgsGdalCredentialOptionsModel::setOptions( const QList< QPair< QString, QString > > &options ) +{ + beginResetModel(); + mCredentialOptions = options; + // last entry should always be a blank entry + if ( mCredentialOptions.isEmpty() || !mCredentialOptions.last().first.isEmpty() ) + mCredentialOptions.append( qMakePair( QString(), QString() ) ); + endResetModel(); + emit optionsChanged(); +} + +void QgsGdalCredentialOptionsModel::setAvailableOptions( const QList &options ) +{ + mAvailableOptions = options; + mDescriptions.clear(); + mAvailableKeys.clear(); + for ( const GdalOption &option : options ) + { + mAvailableKeys.append( option.name ); + mDescriptions[option.name] = option.description; + } +} + +GdalOption QgsGdalCredentialOptionsModel::option( const QString &key ) const +{ + for ( const GdalOption &option : mAvailableOptions ) + { + if ( option.name == key ) + return option; + } + return GdalOption(); +} + +void QgsGdalCredentialOptionsModel::setCredentialOptions( const QList > &options ) +{ + beginResetModel(); + mCredentialOptions = options; + + if ( mCredentialOptions.empty() || !mCredentialOptions.last().first.isEmpty() ) + { + mCredentialOptions.append( qMakePair( QString(), QString() ) ); + } + + endResetModel(); + emit optionsChanged(); +} + + +// +// QgsGdalCredentialOptionsDelegate +// + +QgsGdalCredentialOptionsDelegate::QgsGdalCredentialOptionsDelegate( QObject *parent ) + : QStyledItemDelegate( parent ) +{ + +} + +QWidget *QgsGdalCredentialOptionsDelegate::createEditor( QWidget *parent, const QStyleOptionViewItem &, const QModelIndex &index ) const +{ + switch ( index.column() ) + { + case QgsGdalCredentialOptionsModel::Column::Key: + { + const QgsGdalCredentialOptionsModel *model = qgis::down_cast< const QgsGdalCredentialOptionsModel * >( index.model() ); + QComboBox *combo = new QComboBox( parent ); + if ( !model->availableKeys().isEmpty() ) + { + combo->addItems( model->availableKeys() ); + } + return combo; + } + + case QgsGdalCredentialOptionsModel::Column::Value: + { + // need to find out key for this row + const QgsGdalCredentialOptionsModel *model = qgis::down_cast< const QgsGdalCredentialOptionsModel * >( index.model() ); + const QString key = index.model()->data( model->index( index.row(), QgsGdalCredentialOptionsModel::Column::Key ), Qt::EditRole ).toString(); + if ( key.isEmpty() ) + return nullptr; + + const GdalOption option = model->option( key ); + switch ( option.type ) + { + case GdalOption::Type::Select: + { + QComboBox *cb = new QComboBox( parent ); + for ( const QString &val : std::as_const( option.options ) ) + { + cb->addItem( val, val ); + } + cb->setCurrentIndex( 0 ); + cb->setToolTip( option.description ); + return cb; + } + + case GdalOption::Type::Boolean: + { + QComboBox *cb = new QComboBox( parent ); + cb->addItem( tr( "Yes" ), "YES" ); + cb->addItem( tr( "No" ), "NO" ); + cb->setCurrentIndex( 0 ); + cb->setToolTip( option.description ); + return cb; + } + + case GdalOption::Type::Text: + { + QLineEdit *res = new QLineEdit( parent ); + res->setToolTip( option.description ); + return res; + } + } + return nullptr; + } + + default: + break; + } + return nullptr; +} + +void QgsGdalCredentialOptionsDelegate::setEditorData( QWidget *editor, const QModelIndex &index ) const +{ + switch ( index.column() ) + { + case QgsGdalCredentialOptionsModel::Column::Key: + { + if ( QComboBox *combo = qobject_cast< QComboBox * >( editor ) ) + { + combo->setCurrentIndex( combo->findText( index.data( Qt::EditRole ).toString() ) ); + } + return; + } + + case QgsGdalCredentialOptionsModel::Column::Value: + { + if ( QComboBox *combo = qobject_cast< QComboBox * >( editor ) ) + { + combo->setCurrentIndex( combo->findData( index.data( Qt::EditRole ).toString() ) ); + if ( combo->currentIndex() < 0 ) + combo->setCurrentIndex( 0 ); + } + else if ( QLineEdit *edit = qobject_cast< QLineEdit * >( editor ) ) + { + edit->setText( index.data( Qt::EditRole ).toString() ); + } + return; + } + + default: + break; + } + QStyledItemDelegate::setEditorData( editor, index ); +} + +void QgsGdalCredentialOptionsDelegate::setModelData( QWidget *editor, QAbstractItemModel *model, const QModelIndex &index ) const +{ + switch ( index.column() ) + { + case QgsGdalCredentialOptionsModel::Column::Key: + { + if ( QComboBox *combo = qobject_cast< QComboBox * >( editor ) ) + { + model->setData( index, combo->currentText() ); + } + break; + } + + case QgsGdalCredentialOptionsModel::Column::Value: + { + if ( QComboBox *combo = qobject_cast< QComboBox * >( editor ) ) + { + model->setData( index, combo->currentData() ); + } + else if ( QLineEdit *edit = qobject_cast< QLineEdit * >( editor ) ) + { + model->setData( index, edit->text() ); + } + break; + } + + default: + break; + } +} + + +// +// QgsGdalCredentialOptionsRemoveOptionDelegate +// + +QgsGdalCredentialOptionsRemoveOptionDelegate::QgsGdalCredentialOptionsRemoveOptionDelegate( QObject *parent ) + : QStyledItemDelegate( parent ) +{ + +} + +bool QgsGdalCredentialOptionsRemoveOptionDelegate::eventFilter( QObject *obj, QEvent *event ) +{ + if ( event->type() == QEvent::HoverEnter || event->type() == QEvent::HoverMove ) + { + QHoverEvent *hoverEvent = static_cast( event ); + if ( QAbstractItemView *view = qobject_cast( obj->parent() ) ) + { + const QModelIndex indexUnderMouse = view->indexAt( hoverEvent->pos() ); + setHoveredIndex( indexUnderMouse ); + view->viewport()->update(); + } + } + else if ( event->type() == QEvent::HoverLeave ) + { + setHoveredIndex( QModelIndex() ); + qobject_cast< QWidget * >( obj )->update(); + } + return QStyledItemDelegate::eventFilter( obj, event ); +} + +void QgsGdalCredentialOptionsRemoveOptionDelegate::paint( QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index ) const +{ + QStyledItemDelegate::paint( painter, option, index ); + + if ( index == mHoveredIndex ) + { + QStyleOptionButton buttonOption; + buttonOption.initFrom( option.widget ); + buttonOption.rect = option.rect; + + option.widget->style()->drawControl( QStyle::CE_PushButton, &buttonOption, painter ); + } + + const QIcon icon = QgsApplication::getThemeIcon( "/mIconClearItem.svg" ); + const QRect iconRect( option.rect.left() + ( option.rect.width() - 16 ) / 2, + option.rect.top() + ( option.rect.height() - 16 ) / 2, + 16, 16 ); + + icon.paint( painter, iconRect ); +} + +void QgsGdalCredentialOptionsRemoveOptionDelegate::setHoveredIndex( const QModelIndex &index ) +{ + mHoveredIndex = index; +} + + + +// +// QgsGdalCredentialOptionsWidget +// + +QgsGdalCredentialOptionsWidget::QgsGdalCredentialOptionsWidget( QWidget *parent ) + : QWidget( parent ) +{ + setupUi( this ); + + mLabelInfo->setText( tr( "Consult the GDAL documentation for credential options." ).arg( QLatin1String( "https://gdal.org/user/virtual_file_systems.html#drivers-supporting-virtual-file-systems" ) ) ); + mLabelInfo->setTextInteractionFlags( Qt::TextBrowserInteraction ); + mLabelInfo->setOpenExternalLinks( true ); + + mLabelWarning->setText( tr( "Potentially sensitive credentials are configured! These will be stored in plain text within the QGIS project." ) ); + mLabelWarning->setVisible( false ); + + mModel = new QgsGdalCredentialOptionsModel( this ); + mTableView->setModel( mModel ); + + mTableView->horizontalHeader()->setVisible( false ); + mTableView->verticalHeader()->setVisible( false ); + mTableView->setEditTriggers( QAbstractItemView::AllEditTriggers ); + + mDelegate = new QgsGdalCredentialOptionsDelegate( mTableView ); + mTableView->setItemDelegateForColumn( QgsGdalCredentialOptionsModel::Column::Key, mDelegate ); + mTableView->setItemDelegateForColumn( QgsGdalCredentialOptionsModel::Column::Value, mDelegate ); + mTableView->horizontalHeader()->resizeSection( QgsGdalCredentialOptionsModel::Column::Actions, QFontMetrics( mTableView->font() ).horizontalAdvance( '0' ) * 5 ); + mTableView->horizontalHeader()->setSectionResizeMode( QgsGdalCredentialOptionsModel::Column::Value, QHeaderView::ResizeMode::Stretch ); + + QgsGdalCredentialOptionsRemoveOptionDelegate *removeDelegate = new QgsGdalCredentialOptionsRemoveOptionDelegate( mTableView ); + mTableView->setItemDelegateForColumn( QgsGdalCredentialOptionsModel::Column::Actions, removeDelegate ); + mTableView->viewport()->installEventFilter( removeDelegate ); + connect( mTableView, &QTableView::clicked, this, [this]( const QModelIndex & index ) + { + if ( index.column() == QgsGdalCredentialOptionsModel::Column::Actions ) + { + mModel->removeRows( index.row(), 1 ); + } + } ); + + mModel->setOptions( {} ); + + connect( mModel, &QgsGdalCredentialOptionsModel::optionsChanged, this, &QgsGdalCredentialOptionsWidget::modelOptionsChanged ); +} + +void QgsGdalCredentialOptionsWidget::setDriver( const QString &driver ) +{ + if ( driver == mDriver ) + return; + + mDriver = driver; + + if ( !isProtocolCloudType( mDriver ) ) + { + mModel->setAvailableOptions( {} ); + return; + } + + const QString vsiPrefix = QStringLiteral( "/%1/" ).arg( mDriver ); + const char *pszVsiOptions( VSIGetFileSystemOptions( vsiPrefix.toLocal8Bit().constData() ) ); + if ( !pszVsiOptions ) + return; + + CPLXMLNode *psDoc = CPLParseXMLString( pszVsiOptions ); + if ( !psDoc ) + return; + CPLXMLNode *psOptionList = CPLGetXMLNode( psDoc, "=Options" ); + if ( !psOptionList ) + { + CPLDestroyXMLNode( psDoc ); + return; + } + + int maxKeyLength = 0; + QList< GdalOption > options; + for ( auto psItem = psOptionList->psChild; psItem != nullptr; psItem = psItem->psNext ) + { + if ( psItem->eType != CXT_Element || !EQUAL( psItem->pszValue, "Option" ) ) + continue; + + const QString optionName( CPLGetXMLValue( psItem, "name", nullptr ) ); + if ( optionName.isEmpty() ) + continue; + + GdalOption option; + option.name = optionName; + if ( optionName.length() > maxKeyLength ) + maxKeyLength = optionName.length(); + + option.description = QString( CPLGetXMLValue( psItem, "description", nullptr ) ); + + option.type = GdalOption::Type::Text; + + const char *pszType = CPLGetXMLValue( psItem, "type", nullptr ); + if ( pszType && EQUAL( pszType, "string-select" ) ) + { + option.type = GdalOption::Type::Select; + for ( auto psOption = psItem->psChild; psOption != nullptr; psOption = psOption->psNext ) + { + if ( psOption->eType != CXT_Element || + !EQUAL( psOption->pszValue, "Value" ) || + psOption->psChild == nullptr ) + { + continue; + } + option.options << psOption->psChild->pszValue; + } + option.defaultValue = option.options.value( 0 ); + } + else if ( pszType && EQUAL( pszType, "boolean" ) ) + { + option.type = GdalOption::Type::Boolean; + option.defaultValue = QStringLiteral( "YES" ); + } + + options << option; + } + + CPLDestroyXMLNode( psDoc ); + + mTableView->setColumnWidth( QgsGdalCredentialOptionsModel::Column::Key, static_cast< int >( QFontMetrics( mTableView->font() ).horizontalAdvance( 'X' ) * maxKeyLength * 1.1 ) ); + + mModel->setAvailableOptions( options ); +} + +QVariantMap QgsGdalCredentialOptionsWidget::credentialOptions() const +{ + QVariantMap result; + const QList< QPair< QString, QString > > options = mModel->credentialOptions(); + for ( const QPair< QString, QString> &option : options ) + { + if ( option.first.isEmpty() ) + continue; + + result[option.first] = option.second; + } + + return result; +} + +void QgsGdalCredentialOptionsWidget::setCredentialOptions( const QVariantMap &options ) +{ + QList< QPair< QString, QString > > modelOptions; + for ( auto it = options.constBegin(); it != options.constEnd(); ++it ) + { + modelOptions.append( qMakePair( it.key(), it.value().toString() ) ); + } + mModel->setCredentialOptions( modelOptions ); +} + +void QgsGdalCredentialOptionsWidget::modelOptionsChanged() +{ + bool needsSensitiveWarning = false; + const QVariantMap options = credentialOptions(); + for ( const auto &key : + { "AWS_ACCESS_KEY_ID", + "AWS_SECRET_ACCESS_KEY", + "GS_SECRET_ACCESS_KEY", + "GS_ACCESS_KEY_ID", + "GS_OAUTH2_PRIVATE_KEY", + "GS_OAUTH2_CLIENT_SECRET", + "AZURE_STORAGE_CONNECTION_STRING", + "AZURE_STORAGE_ACCESS_TOKEN", + "AZURE_STORAGE_ACCESS_KEY", + "AZURE_STORAGE_SAS_TOKEN", + "OSS_SECRET_ACCESS_KEY", + "OSS_ACCESS_KEY_ID", + "SWIFT_AUTH_TOKEN", + "SWIFT_KEY" + } ) + { + if ( !options.value( key ).toString().isEmpty() ) + { + needsSensitiveWarning = true; + break; + } + } + mLabelWarning->setVisible( needsSensitiveWarning ); + + emit optionsChanged(); +} diff --git a/src/gui/providers/gdal/qgsgdalcredentialoptionswidget.h b/src/gui/providers/gdal/qgsgdalcredentialoptionswidget.h new file mode 100644 index 000000000000..c59b0ca2cfa3 --- /dev/null +++ b/src/gui/providers/gdal/qgsgdalcredentialoptionswidget.h @@ -0,0 +1,180 @@ +/*************************************************************************** + qgsgdalcredentialoptionswidget.h + ------------------- + begin : June 2024 + copyright : (C) 2024 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +#ifndef QGGDALCREDENTIALOPTIONSWIDGET_H +#define QGGDALCREDENTIALOPTIONSWIDGET_H + +#include "ui_qgsgdalcredentialoptionswidgetbase.h" +#include "qgis_gui.h" +#include "qgis_sip.h" + +#include +#include + +#ifndef SIP_RUN +///@cond PRIVATE + +struct GdalOption +{ + enum class Type + { + Select, + Boolean, + Text + }; + + QString name; + Type type = Type::Text; + QStringList options; + QString description; + QString defaultValue; +}; + +class QgsGdalCredentialOptionsModel : public QAbstractItemModel +{ + Q_OBJECT + + public: + + enum Column + { + Key = 0, + Value = 1, + Actions = 2 + }; + + QgsGdalCredentialOptionsModel( QObject *parent ); + int columnCount( const QModelIndex &parent = QModelIndex() ) const override; + int rowCount( const QModelIndex &parent = QModelIndex() ) const override; + QModelIndex index( int row, int column, const QModelIndex &parent = QModelIndex() ) const override; + QModelIndex parent( const QModelIndex &child ) const override; + Qt::ItemFlags flags( const QModelIndex &index ) const override; + QVariant data( const QModelIndex &index, int role ) const override; + bool setData( const QModelIndex &index, const QVariant &value, int role ) override; + bool insertRows( int position, int rows, const QModelIndex &parent = QModelIndex() ) override; + bool removeRows( int position, int rows, const QModelIndex &parent = QModelIndex() ) override; + + void setOptions( const QList< QPair< QString, QString > > &options ); + void setAvailableOptions( const QList< GdalOption > &options ); + QStringList availableKeys() const { return mAvailableKeys; } + GdalOption option( const QString &key ) const; + QList< QPair< QString, QString > > credentialOptions() const { return mCredentialOptions; } + void setCredentialOptions( const QList< QPair< QString, QString > > &options ); + + signals: + + void optionsChanged(); + + private: + + QList< QPair< QString, QString > > mCredentialOptions; + QList< GdalOption > mAvailableOptions; + QStringList mAvailableKeys; + QMap mDescriptions; +}; + + +class QgsGdalCredentialOptionsDelegate : public QStyledItemDelegate +{ + Q_OBJECT + + public: + + QgsGdalCredentialOptionsDelegate( QObject *parent ); + + protected: + QWidget *createEditor( QWidget *parent, const QStyleOptionViewItem & /*option*/, const QModelIndex &index ) const override; + void setEditorData( QWidget *editor, const QModelIndex &index ) const override; + void setModelData( QWidget *editor, QAbstractItemModel *model, const QModelIndex &index ) const override; +}; + +class QgsGdalCredentialOptionsRemoveOptionDelegate : public QStyledItemDelegate +{ + Q_OBJECT + + public: + QgsGdalCredentialOptionsRemoveOptionDelegate( QObject *parent ); + bool eventFilter( QObject *obj, QEvent *event ) override; + protected: + void paint( QPainter *painter, + const QStyleOptionViewItem &option, const QModelIndex &index ) const override; + private: + void setHoveredIndex( const QModelIndex &index ); + + QModelIndex mHoveredIndex; +}; + +///@endcond PRIVATE +#endif + +/** + * \class QgsGdalCredentialOptionsWidget + * \brief A widget for configuring GDAL credential options. + * + * \since QGIS 3.40 + */ +class GUI_EXPORT QgsGdalCredentialOptionsWidget : public QWidget, private Ui::QgsGdalCredentialOptionsWidgetBase +{ + Q_OBJECT + + public: + + /** + * Constructor for QgsGdalCredentialOptionsWidget, with the specified \a parent widget. + */ + QgsGdalCredentialOptionsWidget( QWidget *parent = nullptr ); + + /** + * Sets the corresponding \a driver. + * + * This should match a GDAL VSI driver, eg "vsis3". + */ + void setDriver( const QString &driver ); + + /** + * Returns the credential options configured in the widget. + * + * \see setCredentialOptions() + */ + QVariantMap credentialOptions() const; + + /** + * Sets the credential \a options to show in the widget. + * + * \see credentialOptions() + */ + void setCredentialOptions( const QVariantMap &options ); + + signals: + + /** + * Emitted when the credential options are changed in the widget. + */ + void optionsChanged(); + + private slots: + + void modelOptionsChanged(); + + private: + + QString mDriver; + QgsGdalCredentialOptionsModel *mModel = nullptr; + QgsGdalCredentialOptionsDelegate *mDelegate = nullptr; + +}; + +#endif // QGGDALCREDENTIALOPTIONSWIDGET_H diff --git a/src/ui/qgsgdalcredentialoptionswidgetbase.ui b/src/ui/qgsgdalcredentialoptionswidgetbase.ui new file mode 100644 index 000000000000..41b50fbca1bb --- /dev/null +++ b/src/ui/qgsgdalcredentialoptionswidgetbase.ui @@ -0,0 +1,50 @@ + + + QgsGdalCredentialOptionsWidgetBase + + + + 0 + 0 + 705 + 446 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + TextLabel + + + + + + + TextLabel + + + + + + + + From ebb7d11a7d4a413f22fad21c6aeb0a6459d2bb07 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 19 Jun 2024 15:15:33 +1000 Subject: [PATCH 05/19] Add credential options to GDAL source select Allows users to configure credentials to use when connecting to GDAL cloud storage providers, e.g. AWS --- .../providers/gdal/qgsgdalsourceselect.cpp | 74 +++++++++++++++++-- src/gui/providers/gdal/qgsgdalsourceselect.h | 8 ++ src/ui/qgsgdalsourceselectbase.ui | 33 ++++----- 3 files changed, 90 insertions(+), 25 deletions(-) diff --git a/src/gui/providers/gdal/qgsgdalsourceselect.cpp b/src/gui/providers/gdal/qgsgdalsourceselect.cpp index c3e9bd0a20cc..6a9e4c9d9b39 100644 --- a/src/gui/providers/gdal/qgsgdalsourceselect.cpp +++ b/src/gui/providers/gdal/qgsgdalsourceselect.cpp @@ -23,6 +23,7 @@ #include "qgsproviderregistry.h" #include "ogr/qgsogrhelperfunctions.h" #include "qgsgdalutils.h" +#include "qgsgdalcredentialoptionswidget.h" #include #include @@ -60,8 +61,6 @@ QgsGdalSourceSelect::QgsGdalSourceSelect( QWidget *parent, Qt::WindowFlags fl, Q cmbProtocolTypes->addItem( protocol.first, protocol.second ); } - mAuthWarning->setText( tr( " Additional credential options are required as documented here." ).arg( QLatin1String( "https://gdal.org/user/virtual_file_systems.html#drivers-supporting-virtual-file-systems" ) ) ); - connect( protocolURI, &QLineEdit::textChanged, this, [ = ]( const QString & text ) { if ( radioSrcProtocol->isChecked() ) @@ -97,6 +96,13 @@ QgsGdalSourceSelect::QgsGdalSourceSelect( QWidget *parent, Qt::WindowFlags fl, Q fillOpenOptions(); } ); mOpenOptionsGroupBox->setVisible( false ); + + mCredentialsWidget = new QgsGdalCredentialOptionsWidget(); + mCredentialOptionsLayout->addWidget( mCredentialsWidget ); + mCredentialOptionsGroupBox->setVisible( false ); + + connect( mCredentialsWidget, &QgsGdalCredentialOptionsWidget::optionsChanged, this, &QgsGdalSourceSelect::credentialOptionsChanged ); + mAuthSettingsProtocol->setDataprovider( QStringLiteral( "gdal" ) ); } @@ -111,7 +117,6 @@ void QgsGdalSourceSelect::setProtocolWidgetsVisibility() mBucket->show(); labelKey->show(); mKey->show(); - mAuthWarning->show(); } else { @@ -122,7 +127,6 @@ void QgsGdalSourceSelect::setProtocolWidgetsVisibility() mBucket->hide(); labelKey->hide(); mKey->hide(); - mAuthWarning->hide(); } } @@ -133,9 +137,9 @@ void QgsGdalSourceSelect::radioSrcFile_toggled( bool checked ) fileGroupBox->show(); protocolGroupBox->hide(); clearOpenOptions(); + updateProtocolOptions(); emit enableButtons( !mFileWidget->filePath().isEmpty() ); - } } @@ -168,6 +172,7 @@ void QgsGdalSourceSelect::radioSrcProtocol_toggled( bool checked ) protocolGroupBox->show(); setProtocolWidgetsVisibility(); clearOpenOptions(); + updateProtocolOptions(); emit enableButtons( !protocolURI->text().isEmpty() ); } @@ -178,6 +183,7 @@ void QgsGdalSourceSelect::cmbProtocolTypes_currentIndexChanged( const QString &t Q_UNUSED( text ) setProtocolWidgetsVisibility(); clearOpenOptions(); + updateProtocolOptions(); } void QgsGdalSourceSelect::addButtonClicked() @@ -306,6 +312,8 @@ void QgsGdalSourceSelect::computeDataSources() } } + const QVariantMap credentialOptions = !mCredentialOptionsGroupBox->isHidden() ? mCredentialOptions : QVariantMap(); + if ( radioSrcFile->isChecked() || radioSrcOgcApi->isChecked() ) { for ( const auto &filePath : QgsFileWidget::splitFilePaths( mRasterPath ) ) @@ -342,6 +350,8 @@ void QgsGdalSourceSelect::computeDataSources() QVariantMap parts; if ( !openOptions.isEmpty() ) parts.insert( QStringLiteral( "openOptions" ), openOptions ); + if ( !credentialOptions.isEmpty() ) + parts.insert( QStringLiteral( "credentialOptions" ), credentialOptions ); parts.insert( QStringLiteral( "path" ), createProtocolURI( cmbProtocolTypes->currentData().toString(), uri, @@ -384,8 +394,33 @@ void QgsGdalSourceSelect::fillOpenOptions() return; } + QVariantMap parts = QgsProviderRegistry::instance()->decodeUri( QStringLiteral( "gdal" ), firstDataSource ); + const QVariantMap credentialOptions = parts.value( QStringLiteral( "credentialOptions" ) ).toMap(); + parts.remove( QStringLiteral( "credentialOptions" ) ); + if ( !credentialOptions.isEmpty() && !vsiPrefix.isEmpty() ) + { + const thread_local QRegularExpression bucketRx( QStringLiteral( "^(.*?)/" ) ); + const QRegularExpressionMatch bucketMatch = bucketRx.match( parts.value( QStringLiteral( "path" ) ).toString() ); + if ( bucketMatch.hasMatch() ) + { + const QString bucket = vsiPrefix + bucketMatch.captured( 1 ); + for ( auto it = credentialOptions.constBegin(); it != credentialOptions.constEnd(); ++it ) + { +#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3, 6, 0) + VSISetPathSpecificOption( bucket.toUtf8().constData(), it.key().toUtf8().constData(), it.value().toString().toUtf8().constData() ); +#elif GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3, 5, 0) + VSISetCredential( bucket.toUtf8().constData(), it.key().toUtf8().constData(), it.value().toString().toUtf8().constData() ); +#else + ( void )bucket; + QgsMessageLog::logMessage( QObject::tr( "Cannot use VSI credential options on GDAL versions earlier than 3.5" ), QStringLiteral( "GDAL" ), Qgis::MessageLevel::Critical ); +#endif + } + } + } + + const QString gdalUri = QgsProviderRegistry::instance()->encodeUri( QStringLiteral( "gdal" ), parts ); GDALDriverH hDriver; - hDriver = GDALIdentifyDriverEx( firstDataSource.toUtf8().toStdString().c_str(), GDAL_OF_RASTER, nullptr, nullptr ); + hDriver = GDALIdentifyDriverEx( gdalUri.toUtf8().toStdString().c_str(), GDAL_OF_RASTER, nullptr, nullptr ); if ( hDriver == nullptr ) return; @@ -458,7 +493,7 @@ void QgsGdalSourceSelect::fillOpenOptions() #if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,8,0) if ( QString( GDALGetDriverShortName( hDriver ) ).compare( QLatin1String( "BAG" ) ) == 0 && label->text() == QLatin1String( "MODE" ) && options.contains( QLatin1String( "INTERPOLATED" ) ) ) { - gdal::dataset_unique_ptr hSrcDS( GDALOpen( firstDataSource.toUtf8().constData(), GA_ReadOnly ) ); + gdal::dataset_unique_ptr hSrcDS( GDALOpen( gdalUri.toUtf8().constData(), GA_ReadOnly ) ); if ( hSrcDS && QString{ GDALGetMetadataItem( hSrcDS.get(), "HAS_SUPERGRIDS", nullptr ) } == QLatin1String( "TRUE" ) ) { idx = cb->findText( QLatin1String( "INTERPOLATED" ) ); @@ -503,7 +538,6 @@ void QgsGdalSourceSelect::fillOpenOptions() } mOpenOptionsGroupBox->setVisible( !mOpenOptionsWidgets.empty() ); - } void QgsGdalSourceSelect::showHelp() @@ -511,4 +545,28 @@ void QgsGdalSourceSelect::showHelp() QgsHelp::openHelp( QStringLiteral( "managing_data_source/opening_data.html#loading-a-layer-from-a-file" ) ); } +void QgsGdalSourceSelect::updateProtocolOptions() +{ + const QString currentProtocol = cmbProtocolTypes->currentData().toString(); + if ( radioSrcProtocol->isChecked() && isProtocolCloudType( currentProtocol ) ) + { + mCredentialsWidget->setDriver( currentProtocol ); + mCredentialOptionsGroupBox->setVisible( true ); + } + else + { + mCredentialOptionsGroupBox->setVisible( false ); + } +} + +void QgsGdalSourceSelect::credentialOptionsChanged() +{ + const QVariantMap newCredentialOptions = mCredentialsWidget->credentialOptions(); + if ( newCredentialOptions == mCredentialOptions ) + return; + + mCredentialOptions = newCredentialOptions; + fillOpenOptions(); +} + ///@endcond diff --git a/src/gui/providers/gdal/qgsgdalsourceselect.h b/src/gui/providers/gdal/qgsgdalsourceselect.h index e90dd6ba3ded..a5a1bf3bc9dc 100644 --- a/src/gui/providers/gdal/qgsgdalsourceselect.h +++ b/src/gui/providers/gdal/qgsgdalsourceselect.h @@ -25,6 +25,8 @@ ///@cond PRIVATE #define SIP_NO_FILE +class QgsGdalCredentialOptionsWidget; + /** * \class QgsGdalSourceSelect * \brief Dialog to select GDAL supported rasters @@ -51,17 +53,23 @@ class QgsGdalSourceSelect : public QgsAbstractDataSourceWidget, private Ui::QgsG private slots: void showHelp(); + void updateProtocolOptions(); + void credentialOptionsChanged(); private: void computeDataSources(); void clearOpenOptions(); void fillOpenOptions(); + std::vector mOpenOptionsWidgets; + QgsGdalCredentialOptionsWidget *mCredentialsWidget = nullptr; QString mRasterPath; QStringList mDataSources; bool mIsOgcApi = false; + QVariantMap mCredentialOptions; + }; ///@endcond diff --git a/src/ui/qgsgdalsourceselectbase.ui b/src/ui/qgsgdalsourceselectbase.ui index 7f7193b864a5..9eb0ca40fbf2 100644 --- a/src/ui/qgsgdalsourceselectbase.ui +++ b/src/ui/qgsgdalsourceselectbase.ui @@ -6,8 +6,8 @@ 0 0 - 355 - 501 + 878 + 921 @@ -179,19 +179,6 @@ - - - - - - - true - - - true - - - @@ -226,6 +213,18 @@ + + + + Credential Options + + + + + + + + @@ -239,8 +238,8 @@ 0 0 - 353 - 68 + 876 + 246 From 21674d8bdd32d1ab45bc3c2069ccaebfdf74f053 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 21 Jun 2024 10:08:22 +1000 Subject: [PATCH 06/19] Handle default credential values --- .../providers/gdal/qgsgdalcredentialoptionswidget.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/gui/providers/gdal/qgsgdalcredentialoptionswidget.cpp b/src/gui/providers/gdal/qgsgdalcredentialoptionswidget.cpp index 46eb85566f49..de736339d792 100644 --- a/src/gui/providers/gdal/qgsgdalcredentialoptionswidget.cpp +++ b/src/gui/providers/gdal/qgsgdalcredentialoptionswidget.cpp @@ -595,6 +595,7 @@ void QgsGdalCredentialOptionsWidget::setDriver( const QString &driver ) option.type = GdalOption::Type::Text; const char *pszType = CPLGetXMLValue( psItem, "type", nullptr ); + const char *pszDefault = CPLGetXMLValue( psItem, "default", nullptr ); if ( pszType && EQUAL( pszType, "string-select" ) ) { option.type = GdalOption::Type::Select; @@ -608,12 +609,18 @@ void QgsGdalCredentialOptionsWidget::setDriver( const QString &driver ) } option.options << psOption->psChild->pszValue; } - option.defaultValue = option.options.value( 0 ); + option.defaultValue = pszDefault ? QString( pszDefault ) : option.options.value( 0 ); } else if ( pszType && EQUAL( pszType, "boolean" ) ) { option.type = GdalOption::Type::Boolean; - option.defaultValue = QStringLiteral( "YES" ); + option.defaultValue = pszDefault ? QString( pszDefault ) : QStringLiteral( "YES" ); + } + else if ( pszType && EQUAL( pszType, "string" ) ) + { + option.type = GdalOption::Type::Text; + if ( pszDefault ) + option.defaultValue = QString( pszDefault ); } options << option; From 0a78dc381497b75efdb781591915bc1175faae29 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 21 Jun 2024 10:23:57 +1000 Subject: [PATCH 07/19] Handle numeric option types --- .../gdal/qgsgdalcredentialoptionswidget.cpp | 111 +++++++++++++++++- .../gdal/qgsgdalcredentialoptionswidget.h | 8 +- 2 files changed, 116 insertions(+), 3 deletions(-) diff --git a/src/gui/providers/gdal/qgsgdalcredentialoptionswidget.cpp b/src/gui/providers/gdal/qgsgdalcredentialoptionswidget.cpp index de736339d792..76af15ee74c9 100644 --- a/src/gui/providers/gdal/qgsgdalcredentialoptionswidget.cpp +++ b/src/gui/providers/gdal/qgsgdalcredentialoptionswidget.cpp @@ -20,6 +20,9 @@ #include "ogr/qgsogrhelperfunctions.h" #include "qgsvariantutils.h" #include "qgsapplication.h" +#include "qgslogger.h" +#include "qgsspinbox.h" +#include "qgsdoublespinbox.h" #include #include @@ -184,7 +187,7 @@ bool QgsGdalCredentialOptionsModel::setData( const QModelIndex &index, const QVa else { if ( option.first != value.toString() ) - option.second = QgsGdalCredentialOptionsModel::option( value.toString() ).defaultValue; + option.second = QgsGdalCredentialOptionsModel::option( value.toString() ).defaultValue.toString(); option.first = value.toString(); if ( wasInvalid ) @@ -367,6 +370,40 @@ QWidget *QgsGdalCredentialOptionsDelegate::createEditor( QWidget *parent, const res->setToolTip( option.description ); return res; } + + case GdalOption::Type::Int: + { + QgsSpinBox *res = new QgsSpinBox( parent ); + res->setToolTip( option.description ); + if ( option.minimum.isValid() ) + res->setMinimum( option.minimum.toInt() ); + else + res->setMinimum( std::numeric_limits< int>::lowest() + 1 ); + if ( option.maximum.isValid() ) + res->setMaximum( option.maximum.toInt() ); + else + res->setMaximum( std::numeric_limits< int>::max() - 1 ); + if ( option.defaultValue.isValid() ) + res->setClearValue( option.defaultValue.toInt() ); + return res; + } + + case GdalOption::Type::Double: + { + QgsDoubleSpinBox *res = new QgsDoubleSpinBox( parent ); + res->setToolTip( option.description ); + if ( option.minimum.isValid() ) + res->setMinimum( option.minimum.toDouble() ); + else + res->setMinimum( std::numeric_limits< double>::lowest() + 1 ); + if ( option.maximum.isValid() ) + res->setMaximum( option.maximum.toDouble() ); + else + res->setMaximum( std::numeric_limits< double>::max() - 1 ); + if ( option.defaultValue.isValid() ) + res->setClearValue( option.defaultValue.toDouble() ); + return res; + } } return nullptr; } @@ -402,6 +439,14 @@ void QgsGdalCredentialOptionsDelegate::setEditorData( QWidget *editor, const QMo { edit->setText( index.data( Qt::EditRole ).toString() ); } + else if ( QgsSpinBox *spin = qobject_cast< QgsSpinBox * >( editor ) ) + { + spin->setValue( index.data( Qt::EditRole ).toInt() ); + } + else if ( QgsDoubleSpinBox *spin = qobject_cast< QgsDoubleSpinBox * >( editor ) ) + { + spin->setValue( index.data( Qt::EditRole ).toDouble() ); + } return; } @@ -434,6 +479,14 @@ void QgsGdalCredentialOptionsDelegate::setModelData( QWidget *editor, QAbstractI { model->setData( index, edit->text() ); } + else if ( QgsSpinBox *spin = qobject_cast< QgsSpinBox * >( editor ) ) + { + model->setData( index, spin->value() ); + } + else if ( QgsDoubleSpinBox *spin = qobject_cast< QgsDoubleSpinBox * >( editor ) ) + { + model->setData( index, spin->value() ); + } break; } @@ -622,6 +675,62 @@ void QgsGdalCredentialOptionsWidget::setDriver( const QString &driver ) if ( pszDefault ) option.defaultValue = QString( pszDefault ); } + else if ( pszType && ( EQUAL( pszType, "int" ) || EQUAL( pszType, "integer" ) ) ) + { + option.type = GdalOption::Type::Int; + if ( pszDefault ) + { + bool ok = false; + const int defaultInt = QString( pszDefault ).toInt( &ok ); + if ( ok ) + option.defaultValue = defaultInt; + } + + if ( const char *pszMin = CPLGetXMLValue( psItem, "min", nullptr ) ) + { + bool ok = false; + const int minInt = QString( pszMin ).toInt( &ok ); + if ( ok ) + option.minimum = minInt; + } + if ( const char *pszMax = CPLGetXMLValue( psItem, "max", nullptr ) ) + { + bool ok = false; + const int maxInt = QString( pszMax ).toInt( &ok ); + if ( ok ) + option.maximum = maxInt; + } + } + else if ( pszType && EQUAL( pszType, "double" ) ) + { + option.type = GdalOption::Type::Double; + if ( pszDefault ) + { + bool ok = false; + const double defaultDouble = QString( pszDefault ).toDouble( &ok ); + if ( ok ) + option.defaultValue = defaultDouble; + } + + if ( const char *pszMin = CPLGetXMLValue( psItem, "min", nullptr ) ) + { + bool ok = false; + const double minDouble = QString( pszMin ).toDouble( &ok ); + if ( ok ) + option.minimum = minDouble; + } + if ( const char *pszMax = CPLGetXMLValue( psItem, "max", nullptr ) ) + { + bool ok = false; + const double maxDouble = QString( pszMax ).toDouble( &ok ); + if ( ok ) + option.maximum = maxDouble; + } + } + else + { + QgsDebugError( QStringLiteral( "Unhandled GDAL option type: %1" ).arg( pszType ) ); + } options << option; } diff --git a/src/gui/providers/gdal/qgsgdalcredentialoptionswidget.h b/src/gui/providers/gdal/qgsgdalcredentialoptionswidget.h index c59b0ca2cfa3..3015dbfb3a48 100644 --- a/src/gui/providers/gdal/qgsgdalcredentialoptionswidget.h +++ b/src/gui/providers/gdal/qgsgdalcredentialoptionswidget.h @@ -33,14 +33,18 @@ struct GdalOption { Select, Boolean, - Text + Text, + Int, + Double }; QString name; Type type = Type::Text; QStringList options; QString description; - QString defaultValue; + QVariant defaultValue; + QVariant minimum; + QVariant maximum; }; class QgsGdalCredentialOptionsModel : public QAbstractItemModel From 18b349719a4206e4965c8a035e0daf283d6332ae Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 21 Jun 2024 10:28:02 +1000 Subject: [PATCH 08/19] Add separator before generic settings --- .../providers/gdal/qgsgdalcredentialoptionswidget.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/gui/providers/gdal/qgsgdalcredentialoptionswidget.cpp b/src/gui/providers/gdal/qgsgdalcredentialoptionswidget.cpp index 76af15ee74c9..2aa0a1bda534 100644 --- a/src/gui/providers/gdal/qgsgdalcredentialoptionswidget.cpp +++ b/src/gui/providers/gdal/qgsgdalcredentialoptionswidget.cpp @@ -324,9 +324,16 @@ QWidget *QgsGdalCredentialOptionsDelegate::createEditor( QWidget *parent, const { const QgsGdalCredentialOptionsModel *model = qgis::down_cast< const QgsGdalCredentialOptionsModel * >( index.model() ); QComboBox *combo = new QComboBox( parent ); - if ( !model->availableKeys().isEmpty() ) + const QStringList availableKeys = model->availableKeys(); + for ( const QString &key : availableKeys ) { - combo->addItems( model->availableKeys() ); + if ( key == QLatin1String( "GDAL_HTTP_MAX_RETRY" ) && combo->count() > 0 ) + { + // add separator before generic settings + combo->insertSeparator( combo->count() ); + } + + combo->addItem( key ); } return combo; } From f328e4314719aa6b765aba70ec82199d8d9459fc Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 21 Jun 2024 10:44:13 +1000 Subject: [PATCH 09/19] Only expose VSI cloud protocols which are actually available --- src/core/qgsgdalutils.cpp | 46 +++++++++++++++++++ src/core/qgsgdalutils.h | 21 +++++++++ .../providers/gdal/qgsgdalsourceselect.cpp | 15 ++---- src/gui/providers/ogr/qgsogrsourceselect.cpp | 19 +++----- 4 files changed, 76 insertions(+), 25 deletions(-) diff --git a/src/core/qgsgdalutils.cpp b/src/core/qgsgdalutils.cpp index dc43074f6607..62cece4db3f0 100644 --- a/src/core/qgsgdalutils.cpp +++ b/src/core/qgsgdalutils.cpp @@ -760,6 +760,52 @@ QStringList QgsGdalUtils::vsiArchivePrefixes() return res; } +QList QgsGdalUtils::vsiNetworkFileSystems() +{ + // get supported extensions + static std::once_flag initialized; + static QList VSI_FILE_SYSTEM_DETAILS; + std::call_once( initialized, [ = ] + { + if ( char **papszPrefixes = VSIGetFileSystemsPrefixes() ) + { + for ( int i = 0; papszPrefixes[i]; i++ ) + { + QgsGdalUtils::VsiNetworkFileSystemDetails details; + details.identifier = QString( papszPrefixes[i] ); + if ( details.identifier.startsWith( '/' ) ) + details.identifier = details.identifier.mid( 1 ); + if ( details.identifier.endsWith( '/' ) ) + details.identifier.chop( 1 ); + + if ( details.identifier == QStringLiteral( "vsicurl" ) ) + details.name = QObject::tr( "HTTP/HTTPS/FTP" ); + else if ( details.identifier == QStringLiteral( "vsis3" ) ) + details.name = QObject::tr( "AWS S3" ); + else if ( details.identifier == QStringLiteral( "vsigs" ) ) + details.name = QObject::tr( "Google Cloud Storage" ); + else if ( details.identifier == QStringLiteral( "vsiaz" ) ) + details.name = QObject::tr( "Microsoft Azure Blob" ); + else if ( details.identifier == QStringLiteral( "vsiadls" ) ) + details.name = QObject::tr( "Microsoft Azure Data Lake Storage" ); + else if ( details.identifier == QStringLiteral( "vsioss" ) ) + details.name = QObject::tr( "Alibaba Cloud OSS" ); + else if ( details.identifier == QStringLiteral( "vsiswift" ) ) + details.name = QObject::tr( "OpenStack Swift Object Storage" ); + else if ( details.identifier == QStringLiteral( "vsihdfs" ) ) + details.name = QObject::tr( "Hadoop File System" ); + else + continue; + VSI_FILE_SYSTEM_DETAILS.append( details ); + } + + CSLDestroy( papszPrefixes ); + } + } ); + + return VSI_FILE_SYSTEM_DETAILS; +} + bool QgsGdalUtils::isVsiArchivePrefix( const QString &prefix ) { return vsiArchivePrefixes().contains( prefix ); diff --git a/src/core/qgsgdalutils.h b/src/core/qgsgdalutils.h index 736046235dfb..28cb566ef42f 100644 --- a/src/core/qgsgdalutils.h +++ b/src/core/qgsgdalutils.h @@ -235,6 +235,27 @@ class CORE_EXPORT QgsGdalUtils */ static QStringList vsiArchivePrefixes(); + /** + * Encapsulates details for a GDAL VSI network file system. + * + * \since QGIS 3.40 + */ + struct VsiNetworkFileSystemDetails + { + //! VSI driver identifier, eg "vsis3" + QString identifier; + + //! Translated, user-friendly name. + QString name; + }; + + /** + * Returns a list of available GDAL VSI network file systems. + * + * \since QGIS 3.40 + */ + static QList< VsiNetworkFileSystemDetails > vsiNetworkFileSystems(); + /** * Returns TRUE if \a prefix is a supported archive style container prefix (e.g. "/vsizip/"). * diff --git a/src/gui/providers/gdal/qgsgdalsourceselect.cpp b/src/gui/providers/gdal/qgsgdalsourceselect.cpp index 6a9e4c9d9b39..db554ce79ec6 100644 --- a/src/gui/providers/gdal/qgsgdalsourceselect.cpp +++ b/src/gui/providers/gdal/qgsgdalsourceselect.cpp @@ -46,19 +46,10 @@ QgsGdalSourceSelect::QgsGdalSourceSelect( QWidget *parent, Qt::WindowFlags fl, Q whileBlocking( radioSrcFile )->setChecked( true ); protocolGroupBox->hide(); - for ( const auto &protocol : - { - std::make_pair( QStringLiteral( "HTTP/HTTPS/FTP" ), QStringLiteral( "vsicurl" ) ), - std::make_pair( QStringLiteral( "AWS S3" ), QStringLiteral( "vsis3" ) ), - std::make_pair( QObject::tr( "Google Cloud Storage" ), QStringLiteral( "vsigs" ) ), - std::make_pair( QObject::tr( "Microsoft Azure Blob" ), QStringLiteral( "vsiaz" ) ), - std::make_pair( QObject::tr( "Microsoft Azure Data Lake Storage" ), QStringLiteral( "vsiadls" ) ), - std::make_pair( QObject::tr( "Alibaba Cloud OSS" ), QStringLiteral( "vsioss" ) ), - std::make_pair( QObject::tr( "OpenStack Swift Object Storage" ), QStringLiteral( "vsiswift" ) ), - std::make_pair( QObject::tr( "Hadoop File System" ), QStringLiteral( "vsihdfs" ) ), - } ) + const QList< QgsGdalUtils::VsiNetworkFileSystemDetails > vsiDetails = QgsGdalUtils::vsiNetworkFileSystems(); + for ( const QgsGdalUtils::VsiNetworkFileSystemDetails &vsiDetail : vsiDetails ) { - cmbProtocolTypes->addItem( protocol.first, protocol.second ); + cmbProtocolTypes->addItem( vsiDetail.name, vsiDetail.identifier ); } connect( protocolURI, &QLineEdit::textChanged, this, [ = ]( const QString & text ) diff --git a/src/gui/providers/ogr/qgsogrsourceselect.cpp b/src/gui/providers/ogr/qgsogrsourceselect.cpp index 66edfb442272..5609135d1f69 100644 --- a/src/gui/providers/ogr/qgsogrsourceselect.cpp +++ b/src/gui/providers/ogr/qgsogrsourceselect.cpp @@ -32,6 +32,7 @@ #include "ogr/qgsnewogrconnection.h" #include "ogr/qgsogrhelperfunctions.h" #include "qgsgui.h" +#include "qgsgdalutils.h" #include #include @@ -95,21 +96,13 @@ QgsOgrSourceSelect::QgsOgrSourceSelect( QWidget *parent, Qt::WindowFlags fl, Qgs } //add protocol drivers - for ( const auto &protocol : - { - std::make_pair( QStringLiteral( "HTTP/HTTPS/FTP" ), QStringLiteral( "vsicurl" ) ), - std::make_pair( QStringLiteral( "AWS S3" ), QStringLiteral( "vsis3" ) ), - std::make_pair( QObject::tr( "Google Cloud Storage" ), QStringLiteral( "vsigs" ) ), - std::make_pair( QObject::tr( "Microsoft Azure Blob" ), QStringLiteral( "vsiaz" ) ), - std::make_pair( QObject::tr( "Microsoft Azure Data Lake Storage" ), QStringLiteral( "vsiadls" ) ), - std::make_pair( QObject::tr( "Alibaba Cloud OSS" ), QStringLiteral( "vsioss" ) ), - std::make_pair( QObject::tr( "OpenStack Swift Object Storage" ), QStringLiteral( "vsiswift" ) ), - std::make_pair( QObject::tr( "Hadoop File System" ), QStringLiteral( "vsihdfs" ) ), - std::make_pair( QObject::tr( "WFS3 (Experimental)" ), QStringLiteral( "WFS3" ) ), - } ) + const QList< QgsGdalUtils::VsiNetworkFileSystemDetails > vsiDetails = QgsGdalUtils::vsiNetworkFileSystems(); + for ( const QgsGdalUtils::VsiNetworkFileSystemDetails &vsiDetail : vsiDetails ) { - cmbProtocolTypes->addItem( protocol.first, protocol.second ); + cmbProtocolTypes->addItem( vsiDetail.name, vsiDetail.identifier ); } + cmbProtocolTypes->addItem( QObject::tr( "WFS3 (Experimental)" ), QStringLiteral( "WFS3" ) ); + cmbDatabaseTypes->blockSignals( false ); cmbConnections->blockSignals( false ); From bfd86b33a42303256b485a576f28adc0d5c24ed9 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 21 Jun 2024 14:58:55 +1000 Subject: [PATCH 10/19] Move option handling out to a common place --- src/core/qgsgdalutils.cpp | 126 ++++++++++++++++ src/core/qgsgdalutils.h | 61 ++++++++ .../gdal/qgsgdalcredentialoptionswidget.cpp | 138 +++--------------- .../gdal/qgsgdalcredentialoptionswidget.h | 27 +--- 4 files changed, 210 insertions(+), 142 deletions(-) diff --git a/src/core/qgsgdalutils.cpp b/src/core/qgsgdalutils.cpp index 62cece4db3f0..744c5ca29ab5 100644 --- a/src/core/qgsgdalutils.cpp +++ b/src/core/qgsgdalutils.cpp @@ -31,6 +31,132 @@ #include #include + +QgsGdalOption QgsGdalOption::fromXmlNode( CPLXMLNode *node ) +{ + if ( node->eType != CXT_Element || !EQUAL( node->pszValue, "Option" ) ) + return {}; + + const QString optionName( CPLGetXMLValue( node, "name", nullptr ) ); + if ( optionName.isEmpty() ) + return {}; + + QgsGdalOption option; + option.name = optionName; + + option.description = QString( CPLGetXMLValue( node, "description", nullptr ) ); + + option.type = QgsGdalOption::Type::Text; + + const char *pszType = CPLGetXMLValue( node, "type", nullptr ); + const char *pszDefault = CPLGetXMLValue( node, "default", nullptr ); + if ( pszType && EQUAL( pszType, "string-select" ) ) + { + option.type = QgsGdalOption::Type::Select; + for ( auto psOption = node->psChild; psOption != nullptr; psOption = psOption->psNext ) + { + if ( psOption->eType != CXT_Element || + !EQUAL( psOption->pszValue, "Value" ) || + psOption->psChild == nullptr ) + { + continue; + } + option.options << psOption->psChild->pszValue; + } + option.defaultValue = pszDefault ? QString( pszDefault ) : option.options.value( 0 ); + return option; + } + else if ( pszType && EQUAL( pszType, "boolean" ) ) + { + option.type = QgsGdalOption::Type::Boolean; + option.defaultValue = pszDefault ? QString( pszDefault ) : QStringLiteral( "YES" ); + return option; + } + else if ( pszType && EQUAL( pszType, "string" ) ) + { + option.type = QgsGdalOption::Type::Text; + if ( pszDefault ) + option.defaultValue = QString( pszDefault ); + return option; + } + else if ( pszType && ( EQUAL( pszType, "int" ) || EQUAL( pszType, "integer" ) ) ) + { + option.type = QgsGdalOption::Type::Int; + if ( pszDefault ) + { + bool ok = false; + const int defaultInt = QString( pszDefault ).toInt( &ok ); + if ( ok ) + option.defaultValue = defaultInt; + } + + if ( const char *pszMin = CPLGetXMLValue( node, "min", nullptr ) ) + { + bool ok = false; + const int minInt = QString( pszMin ).toInt( &ok ); + if ( ok ) + option.minimum = minInt; + } + if ( const char *pszMax = CPLGetXMLValue( node, "max", nullptr ) ) + { + bool ok = false; + const int maxInt = QString( pszMax ).toInt( &ok ); + if ( ok ) + option.maximum = maxInt; + } + return option; + } + else if ( pszType && EQUAL( pszType, "double" ) ) + { + option.type = QgsGdalOption::Type::Double; + if ( pszDefault ) + { + bool ok = false; + const double defaultDouble = QString( pszDefault ).toDouble( &ok ); + if ( ok ) + option.defaultValue = defaultDouble; + } + + if ( const char *pszMin = CPLGetXMLValue( node, "min", nullptr ) ) + { + bool ok = false; + const double minDouble = QString( pszMin ).toDouble( &ok ); + if ( ok ) + option.minimum = minDouble; + } + if ( const char *pszMax = CPLGetXMLValue( node, "max", nullptr ) ) + { + bool ok = false; + const double maxDouble = QString( pszMax ).toDouble( &ok ); + if ( ok ) + option.maximum = maxDouble; + } + return option; + } + + QgsDebugError( QStringLiteral( "Unhandled GDAL option type: %1" ).arg( pszType ) ); + return {}; +} + +QList QgsGdalOption::optionsFromXml( CPLXMLNode *node ) +{ + QList< QgsGdalOption > options; + for ( auto psItem = node->psChild; psItem != nullptr; psItem = node->psNext ) + { + const QgsGdalOption option = fromXmlNode( psItem ); + if ( option.type == QgsGdalOption::Type::Invalid ) + continue; + + options << option; + } + return options; +} + + +// +// QgsGdalUtils +// + bool QgsGdalUtils::supportsRasterCreate( GDALDriverH driver ) { const QString driverShortName = GDALGetDriverShortName( driver ); diff --git a/src/core/qgsgdalutils.h b/src/core/qgsgdalutils.h index 28cb566ef42f..016f504a0360 100644 --- a/src/core/qgsgdalutils.h +++ b/src/core/qgsgdalutils.h @@ -24,6 +24,67 @@ #include "qgsogrutils.h" #include "qgsrasterdataprovider.h" +/** + * \ingroup core + * \class QgsGdalOption + * \brief Encapsulates the definition of a GDAL configuration option. + * + * \note not available in Python bindings + * \since QGIS 3.40 + */ +class CORE_EXPORT QgsGdalOption +{ + public: + + /** + * Option types + */ + enum class Type + { + Invalid, //!< Invalid option + Select, //!< Selection option + Boolean, //!< Boolean option + Text, //!< Text option + Int, //!< Integer option + Double, //!< Double option + }; + + //! Option name + QString name; + + //! Option type + Type type = Type::Invalid; + + //! Option description + QString description; + + //! Available choices, for Select options + QStringList options; + + //! Default value + QVariant defaultValue; + + //! Minimum acceptable value + QVariant minimum; + + //! Maximum acceptable value + QVariant maximum; + + /** + * Creates a QgsGdalOption from an XML \a node. + * + * Returns an invalid option if the node could not be interpreted + * as a GDAL option. + */ + static QgsGdalOption fromXmlNode( CPLXMLNode *node ); + + /** + * Returns a list of all GDAL options from an XML \a node. + */ + static QList< QgsGdalOption > optionsFromXml( CPLXMLNode *node ); +}; + + /** * \ingroup core * \class QgsGdalUtils diff --git a/src/gui/providers/gdal/qgsgdalcredentialoptionswidget.cpp b/src/gui/providers/gdal/qgsgdalcredentialoptionswidget.cpp index 2aa0a1bda534..7998caeaa58c 100644 --- a/src/gui/providers/gdal/qgsgdalcredentialoptionswidget.cpp +++ b/src/gui/providers/gdal/qgsgdalcredentialoptionswidget.cpp @@ -100,7 +100,7 @@ QVariant QgsGdalCredentialOptionsModel::data( const QModelIndex &index, int role return QVariant(); const QPair< QString, QString > option = mCredentialOptions.at( index.row() ); - const GdalOption gdalOption = QgsGdalCredentialOptionsModel::option( option.first ); + const QgsGdalOption gdalOption = QgsGdalCredentialOptionsModel::option( option.first ); switch ( role ) { @@ -112,7 +112,7 @@ QVariant QgsGdalCredentialOptionsModel::data( const QModelIndex &index, int role return option.first; case Column::Value: - return gdalOption.type == GdalOption::Type::Boolean ? ( option.second == QStringLiteral( "YES" ) ? tr( "Yes" ) : option.second == QStringLiteral( "NO" ) ? tr( "No" ) : option.second ) + return gdalOption.type == QgsGdalOption::Type::Boolean ? ( option.second == QStringLiteral( "YES" ) ? tr( "Yes" ) : option.second == QStringLiteral( "NO" ) ? tr( "No" ) : option.second ) : option.second; default: @@ -269,26 +269,26 @@ void QgsGdalCredentialOptionsModel::setOptions( const QList< QPair< QString, QSt emit optionsChanged(); } -void QgsGdalCredentialOptionsModel::setAvailableOptions( const QList &options ) +void QgsGdalCredentialOptionsModel::setAvailableOptions( const QList &options ) { mAvailableOptions = options; mDescriptions.clear(); mAvailableKeys.clear(); - for ( const GdalOption &option : options ) + for ( const QgsGdalOption &option : options ) { mAvailableKeys.append( option.name ); mDescriptions[option.name] = option.description; } } -GdalOption QgsGdalCredentialOptionsModel::option( const QString &key ) const +QgsGdalOption QgsGdalCredentialOptionsModel::option( const QString &key ) const { - for ( const GdalOption &option : mAvailableOptions ) + for ( const QgsGdalOption &option : mAvailableOptions ) { if ( option.name == key ) return option; } - return GdalOption(); + return QgsGdalOption(); } void QgsGdalCredentialOptionsModel::setCredentialOptions( const QList > &options ) @@ -346,10 +346,10 @@ QWidget *QgsGdalCredentialOptionsDelegate::createEditor( QWidget *parent, const if ( key.isEmpty() ) return nullptr; - const GdalOption option = model->option( key ); + const QgsGdalOption option = model->option( key ); switch ( option.type ) { - case GdalOption::Type::Select: + case QgsGdalOption::Type::Select: { QComboBox *cb = new QComboBox( parent ); for ( const QString &val : std::as_const( option.options ) ) @@ -361,7 +361,7 @@ QWidget *QgsGdalCredentialOptionsDelegate::createEditor( QWidget *parent, const return cb; } - case GdalOption::Type::Boolean: + case QgsGdalOption::Type::Boolean: { QComboBox *cb = new QComboBox( parent ); cb->addItem( tr( "Yes" ), "YES" ); @@ -371,14 +371,14 @@ QWidget *QgsGdalCredentialOptionsDelegate::createEditor( QWidget *parent, const return cb; } - case GdalOption::Type::Text: + case QgsGdalOption::Type::Text: { QLineEdit *res = new QLineEdit( parent ); res->setToolTip( option.description ); return res; } - case GdalOption::Type::Int: + case QgsGdalOption::Type::Int: { QgsSpinBox *res = new QgsSpinBox( parent ); res->setToolTip( option.description ); @@ -395,7 +395,7 @@ QWidget *QgsGdalCredentialOptionsDelegate::createEditor( QWidget *parent, const return res; } - case GdalOption::Type::Double: + case QgsGdalOption::Type::Double: { QgsDoubleSpinBox *res = new QgsDoubleSpinBox( parent ); res->setToolTip( option.description ); @@ -634,116 +634,16 @@ void QgsGdalCredentialOptionsWidget::setDriver( const QString &driver ) return; } + const QList< QgsGdalOption > options = QgsGdalOption::optionsFromXml( psOptionList ); + CPLDestroyXMLNode( psDoc ); + int maxKeyLength = 0; - QList< GdalOption > options; - for ( auto psItem = psOptionList->psChild; psItem != nullptr; psItem = psItem->psNext ) + for ( const QgsGdalOption &option : options ) { - if ( psItem->eType != CXT_Element || !EQUAL( psItem->pszValue, "Option" ) ) - continue; - - const QString optionName( CPLGetXMLValue( psItem, "name", nullptr ) ); - if ( optionName.isEmpty() ) - continue; - - GdalOption option; - option.name = optionName; - if ( optionName.length() > maxKeyLength ) - maxKeyLength = optionName.length(); - - option.description = QString( CPLGetXMLValue( psItem, "description", nullptr ) ); - - option.type = GdalOption::Type::Text; - - const char *pszType = CPLGetXMLValue( psItem, "type", nullptr ); - const char *pszDefault = CPLGetXMLValue( psItem, "default", nullptr ); - if ( pszType && EQUAL( pszType, "string-select" ) ) - { - option.type = GdalOption::Type::Select; - for ( auto psOption = psItem->psChild; psOption != nullptr; psOption = psOption->psNext ) - { - if ( psOption->eType != CXT_Element || - !EQUAL( psOption->pszValue, "Value" ) || - psOption->psChild == nullptr ) - { - continue; - } - option.options << psOption->psChild->pszValue; - } - option.defaultValue = pszDefault ? QString( pszDefault ) : option.options.value( 0 ); - } - else if ( pszType && EQUAL( pszType, "boolean" ) ) - { - option.type = GdalOption::Type::Boolean; - option.defaultValue = pszDefault ? QString( pszDefault ) : QStringLiteral( "YES" ); - } - else if ( pszType && EQUAL( pszType, "string" ) ) - { - option.type = GdalOption::Type::Text; - if ( pszDefault ) - option.defaultValue = QString( pszDefault ); - } - else if ( pszType && ( EQUAL( pszType, "int" ) || EQUAL( pszType, "integer" ) ) ) - { - option.type = GdalOption::Type::Int; - if ( pszDefault ) - { - bool ok = false; - const int defaultInt = QString( pszDefault ).toInt( &ok ); - if ( ok ) - option.defaultValue = defaultInt; - } - - if ( const char *pszMin = CPLGetXMLValue( psItem, "min", nullptr ) ) - { - bool ok = false; - const int minInt = QString( pszMin ).toInt( &ok ); - if ( ok ) - option.minimum = minInt; - } - if ( const char *pszMax = CPLGetXMLValue( psItem, "max", nullptr ) ) - { - bool ok = false; - const int maxInt = QString( pszMax ).toInt( &ok ); - if ( ok ) - option.maximum = maxInt; - } - } - else if ( pszType && EQUAL( pszType, "double" ) ) - { - option.type = GdalOption::Type::Double; - if ( pszDefault ) - { - bool ok = false; - const double defaultDouble = QString( pszDefault ).toDouble( &ok ); - if ( ok ) - option.defaultValue = defaultDouble; - } - - if ( const char *pszMin = CPLGetXMLValue( psItem, "min", nullptr ) ) - { - bool ok = false; - const double minDouble = QString( pszMin ).toDouble( &ok ); - if ( ok ) - option.minimum = minDouble; - } - if ( const char *pszMax = CPLGetXMLValue( psItem, "max", nullptr ) ) - { - bool ok = false; - const double maxDouble = QString( pszMax ).toDouble( &ok ); - if ( ok ) - option.maximum = maxDouble; - } - } - else - { - QgsDebugError( QStringLiteral( "Unhandled GDAL option type: %1" ).arg( pszType ) ); - } - - options << option; + if ( option.name.length() > maxKeyLength ) + maxKeyLength = option.name.length(); } - CPLDestroyXMLNode( psDoc ); - mTableView->setColumnWidth( QgsGdalCredentialOptionsModel::Column::Key, static_cast< int >( QFontMetrics( mTableView->font() ).horizontalAdvance( 'X' ) * maxKeyLength * 1.1 ) ); mModel->setAvailableOptions( options ); diff --git a/src/gui/providers/gdal/qgsgdalcredentialoptionswidget.h b/src/gui/providers/gdal/qgsgdalcredentialoptionswidget.h index 3015dbfb3a48..a6a58746b052 100644 --- a/src/gui/providers/gdal/qgsgdalcredentialoptionswidget.h +++ b/src/gui/providers/gdal/qgsgdalcredentialoptionswidget.h @@ -20,6 +20,7 @@ #include "ui_qgsgdalcredentialoptionswidgetbase.h" #include "qgis_gui.h" #include "qgis_sip.h" +#include "qgsgdalutils.h" #include #include @@ -27,26 +28,6 @@ #ifndef SIP_RUN ///@cond PRIVATE -struct GdalOption -{ - enum class Type - { - Select, - Boolean, - Text, - Int, - Double - }; - - QString name; - Type type = Type::Text; - QStringList options; - QString description; - QVariant defaultValue; - QVariant minimum; - QVariant maximum; -}; - class QgsGdalCredentialOptionsModel : public QAbstractItemModel { Q_OBJECT @@ -72,9 +53,9 @@ class QgsGdalCredentialOptionsModel : public QAbstractItemModel bool removeRows( int position, int rows, const QModelIndex &parent = QModelIndex() ) override; void setOptions( const QList< QPair< QString, QString > > &options ); - void setAvailableOptions( const QList< GdalOption > &options ); + void setAvailableOptions( const QList< QgsGdalOption > &options ); QStringList availableKeys() const { return mAvailableKeys; } - GdalOption option( const QString &key ) const; + QgsGdalOption option( const QString &key ) const; QList< QPair< QString, QString > > credentialOptions() const { return mCredentialOptions; } void setCredentialOptions( const QList< QPair< QString, QString > > &options ); @@ -85,7 +66,7 @@ class QgsGdalCredentialOptionsModel : public QAbstractItemModel private: QList< QPair< QString, QString > > mCredentialOptions; - QList< GdalOption > mAvailableOptions; + QList< QgsGdalOption > mAvailableOptions; QStringList mAvailableKeys; QMap mDescriptions; }; From e6f20a280b2b101d6d075170237e9267481b4025 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 21 Jun 2024 15:08:16 +1000 Subject: [PATCH 11/19] Move configuration option widget creation to common place --- src/gui/ogr/qgsogrhelperfunctions.cpp | 81 ++++++++++++++++++- src/gui/ogr/qgsogrhelperfunctions.h | 23 ++++++ .../gdal/qgsgdalcredentialoptionswidget.cpp | 68 +--------------- 3 files changed, 104 insertions(+), 68 deletions(-) diff --git a/src/gui/ogr/qgsogrhelperfunctions.cpp b/src/gui/ogr/qgsogrhelperfunctions.cpp index 312c9a3a85be..d2668c9856e8 100644 --- a/src/gui/ogr/qgsogrhelperfunctions.cpp +++ b/src/gui/ogr/qgsogrhelperfunctions.cpp @@ -20,7 +20,12 @@ #include "qgslogger.h" #include "qgsapplication.h" #include "qgsauthmanager.h" -#include +#include "qgsgdalutils.h" +#include "qgsspinbox.h" +#include "qgsdoublespinbox.h" +#include "qgsfilterlineedit.h" + +#include QString createDatabaseURI( const QString &connectionType, const QString &host, const QString &database, QString port, const QString &configId, QString username, QString password, bool expandAuthConfig ) { @@ -295,3 +300,77 @@ bool isProtocolCloudType( const QString &protocol ) protocol == QLatin1String( "vsiswift" ) || protocol == QLatin1String( "vsihdfs" ) ); } + +QWidget *QgsGdalGuiUtils::createWidgetForOption( const QgsGdalOption &option, QWidget *parent ) +{ + switch ( option.type ) + { + case QgsGdalOption::Type::Select: + { + QComboBox *cb = new QComboBox( parent ); + for ( const QString &val : std::as_const( option.options ) ) + { + cb->addItem( val, val ); + } + cb->setCurrentIndex( 0 ); + cb->setToolTip( option.description ); + return cb; + } + + case QgsGdalOption::Type::Boolean: + { + QComboBox *cb = new QComboBox( parent ); + cb->addItem( QObject::tr( "Yes" ), "YES" ); + cb->addItem( QObject::tr( "No" ), "NO" ); + cb->setCurrentIndex( 0 ); + cb->setToolTip( option.description ); + return cb; + } + + case QgsGdalOption::Type::Text: + { + QgsFilterLineEdit *res = new QgsFilterLineEdit( parent ); + res->setToolTip( option.description ); + res->setShowClearButton( true ); + return res; + } + + case QgsGdalOption::Type::Int: + { + QgsSpinBox *res = new QgsSpinBox( parent ); + res->setToolTip( option.description ); + if ( option.minimum.isValid() ) + res->setMinimum( option.minimum.toInt() ); + else + res->setMinimum( std::numeric_limits< int>::lowest() + 1 ); + if ( option.maximum.isValid() ) + res->setMaximum( option.maximum.toInt() ); + else + res->setMaximum( std::numeric_limits< int>::max() - 1 ); + if ( option.defaultValue.isValid() ) + res->setClearValue( option.defaultValue.toInt() ); + return res; + } + + case QgsGdalOption::Type::Double: + { + QgsDoubleSpinBox *res = new QgsDoubleSpinBox( parent ); + res->setToolTip( option.description ); + if ( option.minimum.isValid() ) + res->setMinimum( option.minimum.toDouble() ); + else + res->setMinimum( std::numeric_limits< double>::lowest() + 1 ); + if ( option.maximum.isValid() ) + res->setMaximum( option.maximum.toDouble() ); + else + res->setMaximum( std::numeric_limits< double>::max() - 1 ); + if ( option.defaultValue.isValid() ) + res->setClearValue( option.defaultValue.toDouble() ); + return res; + } + + case QgsGdalOption::Type::Invalid: + break; + } + return nullptr; +} diff --git a/src/gui/ogr/qgsogrhelperfunctions.h b/src/gui/ogr/qgsogrhelperfunctions.h index 02d5ca68b00b..34ea573b9bf1 100644 --- a/src/gui/ogr/qgsogrhelperfunctions.h +++ b/src/gui/ogr/qgsogrhelperfunctions.h @@ -19,6 +19,9 @@ #include #include "qgis_gui.h" +class QWidget; +class QgsGdalOption; + #define SIP_NO_FILE /** @@ -39,3 +42,23 @@ QString GUI_EXPORT createProtocolURI( const QString &type, const QString &url, c * Returns TRUE if \a protocol (eg "vsis3") is considered a cloud type. */ bool GUI_EXPORT isProtocolCloudType( const QString &protocol ); + +/** + * \ingroup core + * \class QgsGdalOption + * \brief Encapsulates the definition of a GDAL configuration option. + * + * \note not available in Python bindings + * \since QGIS 3.40 + */ +class GUI_EXPORT QgsGdalGuiUtils +{ + public: + + /** + * Creates a new widget for configuration a GDAL \a option. + */ + static QWidget *createWidgetForOption( const QgsGdalOption &option, QWidget *parent = nullptr ); + +}; + diff --git a/src/gui/providers/gdal/qgsgdalcredentialoptionswidget.cpp b/src/gui/providers/gdal/qgsgdalcredentialoptionswidget.cpp index 7998caeaa58c..766fa2c61ded 100644 --- a/src/gui/providers/gdal/qgsgdalcredentialoptionswidget.cpp +++ b/src/gui/providers/gdal/qgsgdalcredentialoptionswidget.cpp @@ -20,7 +20,6 @@ #include "ogr/qgsogrhelperfunctions.h" #include "qgsvariantutils.h" #include "qgsapplication.h" -#include "qgslogger.h" #include "qgsspinbox.h" #include "qgsdoublespinbox.h" @@ -347,72 +346,7 @@ QWidget *QgsGdalCredentialOptionsDelegate::createEditor( QWidget *parent, const return nullptr; const QgsGdalOption option = model->option( key ); - switch ( option.type ) - { - case QgsGdalOption::Type::Select: - { - QComboBox *cb = new QComboBox( parent ); - for ( const QString &val : std::as_const( option.options ) ) - { - cb->addItem( val, val ); - } - cb->setCurrentIndex( 0 ); - cb->setToolTip( option.description ); - return cb; - } - - case QgsGdalOption::Type::Boolean: - { - QComboBox *cb = new QComboBox( parent ); - cb->addItem( tr( "Yes" ), "YES" ); - cb->addItem( tr( "No" ), "NO" ); - cb->setCurrentIndex( 0 ); - cb->setToolTip( option.description ); - return cb; - } - - case QgsGdalOption::Type::Text: - { - QLineEdit *res = new QLineEdit( parent ); - res->setToolTip( option.description ); - return res; - } - - case QgsGdalOption::Type::Int: - { - QgsSpinBox *res = new QgsSpinBox( parent ); - res->setToolTip( option.description ); - if ( option.minimum.isValid() ) - res->setMinimum( option.minimum.toInt() ); - else - res->setMinimum( std::numeric_limits< int>::lowest() + 1 ); - if ( option.maximum.isValid() ) - res->setMaximum( option.maximum.toInt() ); - else - res->setMaximum( std::numeric_limits< int>::max() - 1 ); - if ( option.defaultValue.isValid() ) - res->setClearValue( option.defaultValue.toInt() ); - return res; - } - - case QgsGdalOption::Type::Double: - { - QgsDoubleSpinBox *res = new QgsDoubleSpinBox( parent ); - res->setToolTip( option.description ); - if ( option.minimum.isValid() ) - res->setMinimum( option.minimum.toDouble() ); - else - res->setMinimum( std::numeric_limits< double>::lowest() + 1 ); - if ( option.maximum.isValid() ) - res->setMaximum( option.maximum.toDouble() ); - else - res->setMaximum( std::numeric_limits< double>::max() - 1 ); - if ( option.defaultValue.isValid() ) - res->setClearValue( option.defaultValue.toDouble() ); - return res; - } - } - return nullptr; + return QgsGdalGuiUtils::createWidgetForOption( option, parent ); } default: From 4ca157154c2866343a377c56f01cd7fb58060443 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 21 Jun 2024 15:11:04 +1000 Subject: [PATCH 12/19] Move other GDAL GUI utility functions into QgsGdalGuiUtils --- src/gui/ogr/qgsnewogrconnection.cpp | 17 ++++---- src/gui/ogr/qgsogrhelperfunctions.cpp | 6 +-- src/gui/ogr/qgsogrhelperfunctions.h | 42 +++++++++---------- .../gdal/qgsgdalcredentialoptionswidget.cpp | 2 +- .../providers/gdal/qgsgdalsourceselect.cpp | 16 +++---- src/gui/providers/ogr/qgsogrsourceselect.cpp | 16 +++---- 6 files changed, 48 insertions(+), 51 deletions(-) diff --git a/src/gui/ogr/qgsnewogrconnection.cpp b/src/gui/ogr/qgsnewogrconnection.cpp index c2b16141ada0..2dd5db458fe2 100644 --- a/src/gui/ogr/qgsnewogrconnection.cpp +++ b/src/gui/ogr/qgsnewogrconnection.cpp @@ -97,15 +97,14 @@ QgsNewOgrConnection::QgsNewOgrConnection( QWidget *parent, const QString &connTy void QgsNewOgrConnection::testConnection() { - QString uri; - uri = createDatabaseURI( cmbDatabaseTypes->currentText(), - txtHost->text(), - txtDatabase->text(), - txtPort->text(), - mAuthSettingsDatabase->configId(), - mAuthSettingsDatabase->username(), - mAuthSettingsDatabase->password(), - true ); + QString uri = QgsGdalGuiUtils::createDatabaseURI( cmbDatabaseTypes->currentText(), + txtHost->text(), + txtDatabase->text(), + txtPort->text(), + mAuthSettingsDatabase->configId(), + mAuthSettingsDatabase->username(), + mAuthSettingsDatabase->password(), + true ); QgsDebugMsgLevel( "Connecting using uri = " + uri, 2 ); OGRRegisterAll(); OGRDataSourceH poDS; diff --git a/src/gui/ogr/qgsogrhelperfunctions.cpp b/src/gui/ogr/qgsogrhelperfunctions.cpp index d2668c9856e8..86cb18b49ac1 100644 --- a/src/gui/ogr/qgsogrhelperfunctions.cpp +++ b/src/gui/ogr/qgsogrhelperfunctions.cpp @@ -27,7 +27,7 @@ #include -QString createDatabaseURI( const QString &connectionType, const QString &host, const QString &database, QString port, const QString &configId, QString username, QString password, bool expandAuthConfig ) +QString QgsGdalGuiUtils::createDatabaseURI( const QString &connectionType, const QString &host, const QString &database, QString port, const QString &configId, QString username, QString password, bool expandAuthConfig ) { QString uri; @@ -223,7 +223,7 @@ QString createDatabaseURI( const QString &connectionType, const QString &host, c } -QString createProtocolURI( const QString &type, const QString &url, const QString &configId, const QString &username, const QString &password, bool expandAuthConfig ) +QString QgsGdalGuiUtils::createProtocolURI( const QString &type, const QString &url, const QString &configId, const QString &username, const QString &password, bool expandAuthConfig ) { QString uri; if ( type == QLatin1String( "vsicurl" ) ) @@ -290,7 +290,7 @@ QString createProtocolURI( const QString &type, const QString &url, const QStrin return uri; } -bool isProtocolCloudType( const QString &protocol ) +bool QgsGdalGuiUtils::isProtocolCloudType( const QString &protocol ) { return ( protocol == QLatin1String( "vsis3" ) || protocol == QLatin1String( "vsigs" ) || diff --git a/src/gui/ogr/qgsogrhelperfunctions.h b/src/gui/ogr/qgsogrhelperfunctions.h index 34ea573b9bf1..36ca72d6032e 100644 --- a/src/gui/ogr/qgsogrhelperfunctions.h +++ b/src/gui/ogr/qgsogrhelperfunctions.h @@ -25,28 +25,9 @@ class QgsGdalOption; #define SIP_NO_FILE /** - * CreateDatabaseURI - * \brief Create database uri from connection parameters - * \note not available in python bindings - */ -QString GUI_EXPORT createDatabaseURI( const QString &connectionType, const QString &host, const QString &database, QString port, const QString &configId, QString username, QString password, bool expandAuthConfig = false ); - -/** - * CreateProtocolURI - * \brief Create protocol uri from connection parameters - * \note not available in python bindings - */ -QString GUI_EXPORT createProtocolURI( const QString &type, const QString &url, const QString &configId, const QString &username, const QString &password, bool expandAuthConfig = false ); - -/** - * Returns TRUE if \a protocol (eg "vsis3") is considered a cloud type. - */ -bool GUI_EXPORT isProtocolCloudType( const QString &protocol ); - -/** - * \ingroup core - * \class QgsGdalOption - * \brief Encapsulates the definition of a GDAL configuration option. + * \ingroup gui + * \class QgsGdalGuiUtils + * \brief Utility functions for working with GDAL in GUI classes. * * \note not available in Python bindings * \since QGIS 3.40 @@ -55,6 +36,23 @@ class GUI_EXPORT QgsGdalGuiUtils { public: + /** + * Create database uri from connection parameters + * \note not available in python bindings + */ + static QString createDatabaseURI( const QString &connectionType, const QString &host, const QString &database, QString port, const QString &configId, QString username, QString password, bool expandAuthConfig = false ); + + /** + * Create protocol uri from connection parameters + * \note not available in python bindings + */ + static QString createProtocolURI( const QString &type, const QString &url, const QString &configId, const QString &username, const QString &password, bool expandAuthConfig = false ); + + /** + * Returns TRUE if \a protocol (eg "vsis3") is considered a cloud type. + */ + static bool isProtocolCloudType( const QString &protocol ); + /** * Creates a new widget for configuration a GDAL \a option. */ diff --git a/src/gui/providers/gdal/qgsgdalcredentialoptionswidget.cpp b/src/gui/providers/gdal/qgsgdalcredentialoptionswidget.cpp index 766fa2c61ded..88e4c8e4b537 100644 --- a/src/gui/providers/gdal/qgsgdalcredentialoptionswidget.cpp +++ b/src/gui/providers/gdal/qgsgdalcredentialoptionswidget.cpp @@ -547,7 +547,7 @@ void QgsGdalCredentialOptionsWidget::setDriver( const QString &driver ) mDriver = driver; - if ( !isProtocolCloudType( mDriver ) ) + if ( !QgsGdalGuiUtils::isProtocolCloudType( mDriver ) ) { mModel->setAvailableOptions( {} ); return; diff --git a/src/gui/providers/gdal/qgsgdalsourceselect.cpp b/src/gui/providers/gdal/qgsgdalsourceselect.cpp index db554ce79ec6..13d8b552b527 100644 --- a/src/gui/providers/gdal/qgsgdalsourceselect.cpp +++ b/src/gui/providers/gdal/qgsgdalsourceselect.cpp @@ -99,7 +99,7 @@ QgsGdalSourceSelect::QgsGdalSourceSelect( QWidget *parent, Qt::WindowFlags fl, Q void QgsGdalSourceSelect::setProtocolWidgetsVisibility() { - if ( isProtocolCloudType( cmbProtocolTypes->currentData().toString() ) ) + if ( QgsGdalGuiUtils::isProtocolCloudType( cmbProtocolTypes->currentData().toString() ) ) { labelProtocolURI->hide(); protocolURI->hide(); @@ -318,7 +318,7 @@ void QgsGdalSourceSelect::computeDataSources() } else if ( radioSrcProtocol->isChecked() ) { - bool cloudType = isProtocolCloudType( cmbProtocolTypes->currentData().toString() ); + bool cloudType = QgsGdalGuiUtils::isProtocolCloudType( cmbProtocolTypes->currentData().toString() ); if ( !cloudType && protocolURI->text().isEmpty() ) { return; @@ -344,11 +344,11 @@ void QgsGdalSourceSelect::computeDataSources() if ( !credentialOptions.isEmpty() ) parts.insert( QStringLiteral( "credentialOptions" ), credentialOptions ); parts.insert( QStringLiteral( "path" ), - createProtocolURI( cmbProtocolTypes->currentData().toString(), - uri, - mAuthSettingsProtocol->configId(), - mAuthSettingsProtocol->username(), - mAuthSettingsProtocol->password() ) ); + QgsGdalGuiUtils::createProtocolURI( cmbProtocolTypes->currentData().toString(), + uri, + mAuthSettingsProtocol->configId(), + mAuthSettingsProtocol->username(), + mAuthSettingsProtocol->password() ) ); mDataSources << QgsProviderRegistry::instance()->encodeUri( QStringLiteral( "gdal" ), parts ); } } @@ -539,7 +539,7 @@ void QgsGdalSourceSelect::showHelp() void QgsGdalSourceSelect::updateProtocolOptions() { const QString currentProtocol = cmbProtocolTypes->currentData().toString(); - if ( radioSrcProtocol->isChecked() && isProtocolCloudType( currentProtocol ) ) + if ( radioSrcProtocol->isChecked() && QgsGdalGuiUtils::isProtocolCloudType( currentProtocol ) ) { mCredentialsWidget->setDriver( currentProtocol ); mCredentialOptionsGroupBox->setVisible( true ); diff --git a/src/gui/providers/ogr/qgsogrsourceselect.cpp b/src/gui/providers/ogr/qgsogrsourceselect.cpp index 5609135d1f69..6405a27aeea6 100644 --- a/src/gui/providers/ogr/qgsogrsourceselect.cpp +++ b/src/gui/providers/ogr/qgsogrsourceselect.cpp @@ -284,7 +284,7 @@ void QgsOgrSourceSelect::setSelectedConnection() void QgsOgrSourceSelect::setProtocolWidgetsVisibility() { - if ( isProtocolCloudType( cmbProtocolTypes->currentData().toString() ) ) + if ( QgsGdalGuiUtils::isProtocolCloudType( cmbProtocolTypes->currentData().toString() ) ) { labelProtocolURI->hide(); protocolURI->hide(); @@ -376,7 +376,7 @@ void QgsOgrSourceSelect::computeDataSources( bool interactive ) if ( !openOptions.isEmpty() ) parts.insert( QStringLiteral( "openOptions" ), openOptions ); parts.insert( QStringLiteral( "path" ), - createDatabaseURI( + QgsGdalGuiUtils::createDatabaseURI( cmbDatabaseTypes->currentText(), host, database, @@ -390,7 +390,7 @@ void QgsOgrSourceSelect::computeDataSources( bool interactive ) } else if ( radioSrcProtocol->isChecked() ) { - bool cloudType = isProtocolCloudType( cmbProtocolTypes->currentData().toString() ); + bool cloudType = QgsGdalGuiUtils::isProtocolCloudType( cmbProtocolTypes->currentData().toString() ); if ( !cloudType && protocolURI->text().isEmpty() ) { if ( interactive ) @@ -426,11 +426,11 @@ void QgsOgrSourceSelect::computeDataSources( bool interactive ) if ( !openOptions.isEmpty() ) parts.insert( QStringLiteral( "openOptions" ), openOptions ); parts.insert( QStringLiteral( "path" ), - createProtocolURI( cmbProtocolTypes->currentData().toString(), - uri, - mAuthSettingsProtocol->configId(), - mAuthSettingsProtocol->username(), - mAuthSettingsProtocol->password() ) ); + QgsGdalGuiUtils::createProtocolURI( cmbProtocolTypes->currentData().toString(), + uri, + mAuthSettingsProtocol->configId(), + mAuthSettingsProtocol->username(), + mAuthSettingsProtocol->password() ) ); mDataSources << QgsProviderRegistry::instance()->encodeUri( QStringLiteral( "ogr" ), parts ); } else if ( radioSrcFile->isChecked() || radioSrcOgcApi->isChecked() ) From f9ea6482d2a0f0fbc9a4c9071a1c3ade422cf19a Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 21 Jun 2024 15:57:11 +1000 Subject: [PATCH 13/19] Condense duplicate code for creation of widgets from GDAL options --- src/core/qgsgdalutils.cpp | 7 +- src/core/qgsgdalutils.h | 7 +- src/gui/ogr/qgsogrhelperfunctions.cpp | 40 ++++- src/gui/ogr/qgsogrhelperfunctions.h | 5 +- .../providers/gdal/qgsgdalsourceselect.cpp | 130 ++++++++-------- src/gui/providers/ogr/qgsogrsourceselect.cpp | 139 ++++++++---------- 6 files changed, 174 insertions(+), 154 deletions(-) diff --git a/src/core/qgsgdalutils.cpp b/src/core/qgsgdalutils.cpp index 744c5ca29ab5..f2bf33f81101 100644 --- a/src/core/qgsgdalutils.cpp +++ b/src/core/qgsgdalutils.cpp @@ -32,7 +32,7 @@ #include -QgsGdalOption QgsGdalOption::fromXmlNode( CPLXMLNode *node ) +QgsGdalOption QgsGdalOption::fromXmlNode( const CPLXMLNode *node ) { if ( node->eType != CXT_Element || !EQUAL( node->pszValue, "Option" ) ) return {}; @@ -45,6 +45,7 @@ QgsGdalOption QgsGdalOption::fromXmlNode( CPLXMLNode *node ) option.name = optionName; option.description = QString( CPLGetXMLValue( node, "description", nullptr ) ); + option.scope = QString( CPLGetXMLValue( node, "scope", nullptr ) ); option.type = QgsGdalOption::Type::Text; @@ -138,10 +139,10 @@ QgsGdalOption QgsGdalOption::fromXmlNode( CPLXMLNode *node ) return {}; } -QList QgsGdalOption::optionsFromXml( CPLXMLNode *node ) +QList QgsGdalOption::optionsFromXml( const CPLXMLNode *node ) { QList< QgsGdalOption > options; - for ( auto psItem = node->psChild; psItem != nullptr; psItem = node->psNext ) + for ( auto psItem = node->psChild; psItem != nullptr; psItem = psItem->psNext ) { const QgsGdalOption option = fromXmlNode( psItem ); if ( option.type == QgsGdalOption::Type::Invalid ) diff --git a/src/core/qgsgdalutils.h b/src/core/qgsgdalutils.h index 016f504a0360..6b587a3c177b 100644 --- a/src/core/qgsgdalutils.h +++ b/src/core/qgsgdalutils.h @@ -70,18 +70,21 @@ class CORE_EXPORT QgsGdalOption //! Maximum acceptable value QVariant maximum; + //! Option scope + QString scope; + /** * Creates a QgsGdalOption from an XML \a node. * * Returns an invalid option if the node could not be interpreted * as a GDAL option. */ - static QgsGdalOption fromXmlNode( CPLXMLNode *node ); + static QgsGdalOption fromXmlNode( const CPLXMLNode *node ); /** * Returns a list of all GDAL options from an XML \a node. */ - static QList< QgsGdalOption > optionsFromXml( CPLXMLNode *node ); + static QList< QgsGdalOption > optionsFromXml( const CPLXMLNode *node ); }; diff --git a/src/gui/ogr/qgsogrhelperfunctions.cpp b/src/gui/ogr/qgsogrhelperfunctions.cpp index 86cb18b49ac1..c52528873c2b 100644 --- a/src/gui/ogr/qgsogrhelperfunctions.cpp +++ b/src/gui/ogr/qgsogrhelperfunctions.cpp @@ -301,13 +301,17 @@ bool QgsGdalGuiUtils::isProtocolCloudType( const QString &protocol ) protocol == QLatin1String( "vsihdfs" ) ); } -QWidget *QgsGdalGuiUtils::createWidgetForOption( const QgsGdalOption &option, QWidget *parent ) +QWidget *QgsGdalGuiUtils::createWidgetForOption( const QgsGdalOption &option, QWidget *parent, bool includeDefaultChoices ) { switch ( option.type ) { case QgsGdalOption::Type::Select: { QComboBox *cb = new QComboBox( parent ); + if ( includeDefaultChoices ) + { + cb->addItem( QObject::tr( "" ), QgsVariantUtils::createNullVariant( QMetaType::Type::QString ) ); + } for ( const QString &val : std::as_const( option.options ) ) { cb->addItem( val, val ); @@ -320,6 +324,10 @@ QWidget *QgsGdalGuiUtils::createWidgetForOption( const QgsGdalOption &option, QW case QgsGdalOption::Type::Boolean: { QComboBox *cb = new QComboBox( parent ); + if ( includeDefaultChoices ) + { + cb->addItem( QObject::tr( "" ), QgsVariantUtils::createNullVariant( QMetaType::Type::QString ) ); + } cb->addItem( QObject::tr( "Yes" ), "YES" ); cb->addItem( QObject::tr( "No" ), "NO" ); cb->setCurrentIndex( 0 ); @@ -332,6 +340,10 @@ QWidget *QgsGdalGuiUtils::createWidgetForOption( const QgsGdalOption &option, QW QgsFilterLineEdit *res = new QgsFilterLineEdit( parent ); res->setToolTip( option.description ); res->setShowClearButton( true ); + if ( includeDefaultChoices ) + { + res->setPlaceholderText( QObject::tr( "Default" ) ); + } return res; } @@ -342,13 +354,22 @@ QWidget *QgsGdalGuiUtils::createWidgetForOption( const QgsGdalOption &option, QW if ( option.minimum.isValid() ) res->setMinimum( option.minimum.toInt() ); else - res->setMinimum( std::numeric_limits< int>::lowest() + 1 ); + res->setMinimum( 0 ); if ( option.maximum.isValid() ) res->setMaximum( option.maximum.toInt() ); else res->setMaximum( std::numeric_limits< int>::max() - 1 ); - if ( option.defaultValue.isValid() ) + if ( includeDefaultChoices ) + { + res->setMinimum( res->minimum() - 1 ); + res->setClearValueMode( QgsSpinBox::ClearValueMode::MinimumValue, + QObject::tr( "Default" ) ); + } + else if ( option.defaultValue.isValid() ) + { res->setClearValue( option.defaultValue.toInt() ); + } + res->clear(); return res; } @@ -359,13 +380,24 @@ QWidget *QgsGdalGuiUtils::createWidgetForOption( const QgsGdalOption &option, QW if ( option.minimum.isValid() ) res->setMinimum( option.minimum.toDouble() ); else - res->setMinimum( std::numeric_limits< double>::lowest() + 1 ); + res->setMinimum( 0 ); if ( option.maximum.isValid() ) res->setMaximum( option.maximum.toDouble() ); else res->setMaximum( std::numeric_limits< double>::max() - 1 ); if ( option.defaultValue.isValid() ) res->setClearValue( option.defaultValue.toDouble() ); + if ( includeDefaultChoices ) + { + res->setMinimum( res->minimum() - 1 ); + res->setClearValueMode( QgsDoubleSpinBox::ClearValueMode::MinimumValue, + QObject::tr( "Default" ) ); + } + else if ( option.defaultValue.isValid() ) + { + res->setClearValue( option.defaultValue.toDouble() ); + } + res->clear(); return res; } diff --git a/src/gui/ogr/qgsogrhelperfunctions.h b/src/gui/ogr/qgsogrhelperfunctions.h index 36ca72d6032e..ecea077a997a 100644 --- a/src/gui/ogr/qgsogrhelperfunctions.h +++ b/src/gui/ogr/qgsogrhelperfunctions.h @@ -55,8 +55,11 @@ class GUI_EXPORT QgsGdalGuiUtils /** * Creates a new widget for configuration a GDAL \a option. + * + * If \a includeDefaultChoices is TRUE then the widget will include an option + * for the default value. */ - static QWidget *createWidgetForOption( const QgsGdalOption &option, QWidget *parent = nullptr ); + static QWidget *createWidgetForOption( const QgsGdalOption &option, QWidget *parent = nullptr, bool includeDefaultChoices = false ); }; diff --git a/src/gui/providers/gdal/qgsgdalsourceselect.cpp b/src/gui/providers/gdal/qgsgdalsourceselect.cpp index 13d8b552b527..5dc8130007e2 100644 --- a/src/gui/providers/gdal/qgsgdalsourceselect.cpp +++ b/src/gui/providers/gdal/qgsgdalsourceselect.cpp @@ -24,6 +24,8 @@ #include "ogr/qgsogrhelperfunctions.h" #include "qgsgdalutils.h" #include "qgsgdalcredentialoptionswidget.h" +#include "qgsspinbox.h" +#include "qgsdoublespinbox.h" #include #include @@ -270,10 +272,32 @@ bool QgsGdalSourceSelect::configureFromUri( const QString &uri ) cb->setCurrentIndex( idx ); } } - else if ( auto le = qobject_cast( *widget ) ) + else if ( QLineEdit *le = qobject_cast( *widget ) ) { le->setText( opt.value().toString() ); } + else if ( QgsSpinBox *intSpin = qobject_cast( *widget ) ) + { + if ( opt.value().toString().isEmpty() ) + { + intSpin->clear(); + } + else + { + intSpin->setValue( opt.value().toInt() ); + } + } + else if ( QgsDoubleSpinBox *doubleSpin = qobject_cast( *widget ) ) + { + if ( opt.value().toString().isEmpty() ) + { + doubleSpin->clear(); + } + else + { + doubleSpin->setValue( opt.value().toDouble() ); + } + } } } } @@ -297,9 +321,23 @@ void QgsGdalSourceSelect::computeDataSources() { value = le->text(); } + else if ( QgsSpinBox *intSpin = qobject_cast( control ) ) + { + if ( intSpin->value() != intSpin->clearValue() ) + { + value = QString::number( intSpin->value() ); + } + } + else if ( QgsDoubleSpinBox *doubleSpin = qobject_cast( control ) ) + { + if ( doubleSpin->value() != doubleSpin->clearValue() ) + { + value = QString::number( doubleSpin->value() ); + } + } if ( !value.isEmpty() ) { - openOptions << QStringLiteral( "%1=%2" ).arg( control->objectName() ).arg( value ); + openOptions << QStringLiteral( "%1=%2" ).arg( control->objectName(), value ); } } @@ -429,91 +467,45 @@ void QgsGdalSourceSelect::fillOpenOptions() return; } - for ( auto psItem = psOpenOptionList->psChild; psItem != nullptr; psItem = psItem->psNext ) - { - if ( psItem->eType != CXT_Element || !EQUAL( psItem->pszValue, "Option" ) ) - continue; + const QList< QgsGdalOption > options = QgsGdalOption::optionsFromXml( psOpenOptionList ); + CPLDestroyXMLNode( psDoc ); - const char *pszOptionName = CPLGetXMLValue( psItem, "name", nullptr ); - if ( pszOptionName == nullptr ) + for ( const QgsGdalOption &option : options ) + { + // Exclude options that are not of raster scope + if ( !option.scope.isEmpty() + && option.scope.compare( QLatin1String( "raster" ), Qt::CaseInsensitive ) != 0 ) continue; - // Exclude options that are not of raster scope - const char *pszScope = CPLGetXMLValue( psItem, "scope", nullptr ); - if ( pszScope != nullptr && strstr( pszScope, "raster" ) == nullptr ) + QWidget *control = QgsGdalGuiUtils::createWidgetForOption( option, nullptr, true ); + if ( !control ) continue; - const char *pszType = CPLGetXMLValue( psItem, "type", nullptr ); - QStringList options; - if ( pszType && EQUAL( pszType, "string-select" ) ) +#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,8,0) + if ( QString( GDALGetDriverShortName( hDriver ) ).compare( QLatin1String( "BAG" ) ) == 0 + && option.name == QLatin1String( "MODE" ) && option.options.contains( QLatin1String( "INTERPOLATED" ) ) ) { - for ( auto psOption = psItem->psChild; psOption != nullptr; psOption = psOption->psNext ) + gdal::dataset_unique_ptr hSrcDS( GDALOpen( gdalUri.toUtf8().constData(), GA_ReadOnly ) ); + if ( hSrcDS && QString{ GDALGetMetadataItem( hSrcDS.get(), "HAS_SUPERGRIDS", nullptr ) } == QLatin1String( "TRUE" ) ) { - if ( psOption->eType != CXT_Element || - !EQUAL( psOption->pszValue, "Value" ) || - psOption->psChild == nullptr ) + if ( QComboBox *combo = qobject_cast< QComboBox * >( control ) ) { - continue; + combo->setCurrentIndex( combo->findText( QLatin1String( "INTERPOLATED" ) ) ); } - options << psOption->psChild->pszValue; } } - - QLabel *label = new QLabel( pszOptionName ); - QWidget *control = nullptr; - if ( pszType && EQUAL( pszType, "boolean" ) ) - { - QComboBox *cb = new QComboBox(); - cb->addItem( tr( "Yes" ), "YES" ); - cb->addItem( tr( "No" ), "NO" ); - cb->addItem( tr( "" ), QgsVariantUtils::createNullVariant( QMetaType::Type::QString ) ); - int idx = cb->findData( QgsVariantUtils::createNullVariant( QMetaType::Type::QString ) ); - cb->setCurrentIndex( idx ); - control = cb; - } - else if ( !options.isEmpty() ) - { - QComboBox *cb = new QComboBox(); - for ( const QString &val : std::as_const( options ) ) - { - cb->addItem( val, val ); - } - cb->addItem( tr( "" ), QgsVariantUtils::createNullVariant( QMetaType::Type::QString ) ); - int idx = cb->findData( QgsVariantUtils::createNullVariant( QMetaType::Type::QString ) ); - -#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,8,0) - if ( QString( GDALGetDriverShortName( hDriver ) ).compare( QLatin1String( "BAG" ) ) == 0 && label->text() == QLatin1String( "MODE" ) && options.contains( QLatin1String( "INTERPOLATED" ) ) ) - { - gdal::dataset_unique_ptr hSrcDS( GDALOpen( gdalUri.toUtf8().constData(), GA_ReadOnly ) ); - if ( hSrcDS && QString{ GDALGetMetadataItem( hSrcDS.get(), "HAS_SUPERGRIDS", nullptr ) } == QLatin1String( "TRUE" ) ) - { - idx = cb->findText( QLatin1String( "INTERPOLATED" ) ); - } - } #endif - cb->setCurrentIndex( idx ); - control = cb; - } - else - { - QLineEdit *le = new QLineEdit( ); - control = le; - } - control->setObjectName( pszOptionName ); + control->setObjectName( option.name ); mOpenOptionsWidgets.push_back( control ); - const char *pszDescription = CPLGetXMLValue( psItem, "description", nullptr ); - if ( pszDescription ) - { - label->setToolTip( QStringLiteral( "

%1

" ).arg( pszDescription ) ); - control->setToolTip( QStringLiteral( "

%1

" ).arg( pszDescription ) ); - } + QLabel *label = new QLabel( option.name ); + if ( !option.description.isEmpty() ) + label->setToolTip( QStringLiteral( "

%1

" ).arg( option.description ) ); + mOpenOptionsLayout->addRow( label, control ); } - CPLDestroyXMLNode( psDoc ); - // Set label to point to driver help page const char *pszHelpTopic = GDALGetMetadataItem( hDriver, GDAL_DMD_HELPTOPIC, nullptr ); if ( pszHelpTopic ) diff --git a/src/gui/providers/ogr/qgsogrsourceselect.cpp b/src/gui/providers/ogr/qgsogrsourceselect.cpp index 6405a27aeea6..2aa0b6632b8f 100644 --- a/src/gui/providers/ogr/qgsogrsourceselect.cpp +++ b/src/gui/providers/ogr/qgsogrsourceselect.cpp @@ -33,6 +33,8 @@ #include "ogr/qgsogrhelperfunctions.h" #include "qgsgui.h" #include "qgsgdalutils.h" +#include "qgsspinbox.h" +#include "qgsdoublespinbox.h" #include #include @@ -325,9 +327,23 @@ void QgsOgrSourceSelect::computeDataSources( bool interactive ) { value = le->text(); } + else if ( QgsSpinBox *intSpin = qobject_cast( control ) ) + { + if ( intSpin->value() != intSpin->clearValue() ) + { + value = QString::number( intSpin->value() ); + } + } + else if ( QgsDoubleSpinBox *doubleSpin = qobject_cast( control ) ) + { + if ( doubleSpin->value() != doubleSpin->clearValue() ) + { + value = QString::number( doubleSpin->value() ); + } + } if ( !value.isEmpty() ) { - openOptions << QStringLiteral( "%1=%2" ).arg( control->objectName() ).arg( value ); + openOptions << QStringLiteral( "%1=%2" ).arg( control->objectName(), value ); } } @@ -673,6 +689,28 @@ bool QgsOgrSourceSelect::configureFromUri( const QString &uri ) { le->setText( opt.value().toString() ); } + else if ( QgsSpinBox *intSpin = qobject_cast( *widget ) ) + { + if ( opt.value().toString().isEmpty() ) + { + intSpin->clear(); + } + else + { + intSpin->setValue( opt.value().toInt() ); + } + } + else if ( QgsDoubleSpinBox *doubleSpin = qobject_cast( *widget ) ) + { + if ( opt.value().toString().isEmpty() ) + { + doubleSpin->clear(); + } + else + { + doubleSpin->setValue( opt.value().toDouble() ); + } + } } } } @@ -722,42 +760,38 @@ void QgsOgrSourceSelect::fillOpenOptions() return; } + const QList< QgsGdalOption > options = QgsGdalOption::optionsFromXml( psOpenOptionList ); + CPLDestroyXMLNode( psDoc ); + const bool bIsGPKG = EQUAL( GDALGetDriverShortName( hDriver ), "GPKG" ); - for ( auto psItem = psOpenOptionList->psChild; psItem != nullptr; psItem = psItem->psNext ) + for ( const QgsGdalOption &option : options ) { - if ( psItem->eType != CXT_Element || !EQUAL( psItem->pszValue, "Option" ) ) - continue; - - const char *pszOptionName = CPLGetXMLValue( psItem, "name", nullptr ); - if ( pszOptionName == nullptr ) - continue; - // Exclude options that are not of vector scope - const char *pszScope = CPLGetXMLValue( psItem, "scope", nullptr ); - if ( pszScope != nullptr && strstr( pszScope, "vector" ) == nullptr ) + if ( !option.scope.isEmpty() + && option.scope.compare( QLatin1String( "vector" ), Qt::CaseInsensitive ) != 0 ) continue; // The GPKG driver list a lot of options that are only for rasters if ( bIsGPKG && strstr( pszOpenOptionList, "scope=" ) == nullptr && - !EQUAL( pszOptionName, "LIST_ALL_TABLES" ) && - !EQUAL( pszOptionName, "PRELUDE_STATEMENTS" ) ) + option.name != QLatin1String( "LIST_ALL_TABLES" ) && + option.name != QLatin1String( "PRELUDE_STATEMENTS" ) ) continue; // The NOLOCK option is automatically set by the OGR provider. Do not // expose it - if ( bIsGPKG && EQUAL( pszOptionName, "NOLOCK" ) ) + if ( bIsGPKG && option.name == QLatin1String( "NOLOCK" ) ) continue; // Do not list database options already asked in the database dialog if ( radioSrcDatabase->isChecked() && - ( EQUAL( pszOptionName, "USER" ) || - EQUAL( pszOptionName, "PASSWORD" ) || - EQUAL( pszOptionName, "HOST" ) || - EQUAL( pszOptionName, "DBNAME" ) || - EQUAL( pszOptionName, "DATABASE" ) || - EQUAL( pszOptionName, "PORT" ) || - EQUAL( pszOptionName, "SERVICE" ) ) ) + ( option.name == QLatin1String( "USER" ) || + option.name == QLatin1String( "PASSWORD" ) || + option.name == QLatin1String( "HOST" ) || + option.name == QLatin1String( "DBNAME" ) || + option.name == QLatin1String( "DATABASE" ) || + option.name == QLatin1String( "PORT" ) || + option.name == QLatin1String( "SERVICE" ) ) ) { continue; } @@ -765,68 +799,23 @@ void QgsOgrSourceSelect::fillOpenOptions() // QGIS data model doesn't support the OGRFeature native data concept // (typically used for GeoJSON "foreign" members). Hide it to avoid setting // wrong expectations to users (https://github.com/qgis/QGIS/issues/48004) - if ( EQUAL( pszOptionName, "NATIVE_DATA" ) ) + if ( option.name == QLatin1String( "NATIVE_DATA" ) ) continue; - const char *pszType = CPLGetXMLValue( psItem, "type", nullptr ); - QStringList options; - if ( pszType && EQUAL( pszType, "string-select" ) ) - { - for ( auto psOption = psItem->psChild; psOption != nullptr; psOption = psOption->psNext ) - { - if ( psOption->eType != CXT_Element || - !EQUAL( psOption->pszValue, "Value" ) || - psOption->psChild == nullptr ) - { - continue; - } - options << psOption->psChild->pszValue; - } - } + QWidget *control = QgsGdalGuiUtils::createWidgetForOption( option, nullptr, true ); + if ( !control ) + continue; - QLabel *label = new QLabel( pszOptionName ); - QWidget *control = nullptr; - if ( pszType && EQUAL( pszType, "boolean" ) ) - { - QComboBox *cb = new QComboBox(); - cb->addItem( tr( "Yes" ), "YES" ); - cb->addItem( tr( "No" ), "NO" ); - cb->addItem( tr( "" ), QgsVariantUtils::createNullVariant( QMetaType::Type::QString ) ); - int idx = cb->findData( QgsVariantUtils::createNullVariant( QMetaType::Type::QString ) ); - cb->setCurrentIndex( idx ); - control = cb; - } - else if ( !options.isEmpty() ) - { - QComboBox *cb = new QComboBox(); - for ( const QString &val : std::as_const( options ) ) - { - cb->addItem( val, val ); - } - cb->addItem( tr( "" ), QgsVariantUtils::createNullVariant( QMetaType::Type::QString ) ); - int idx = cb->findData( QgsVariantUtils::createNullVariant( QMetaType::Type::QString ) ); - cb->setCurrentIndex( idx ); - control = cb; - } - else - { - QLineEdit *le = new QLineEdit( ); - control = le; - } - control->setObjectName( pszOptionName ); + control->setObjectName( option.name ); mOpenOptionsWidgets.push_back( control ); - const char *pszDescription = CPLGetXMLValue( psItem, "description", nullptr ); - if ( pszDescription ) - { - label->setToolTip( QStringLiteral( "

%1

" ).arg( pszDescription ) ); - control->setToolTip( QStringLiteral( "

%1

" ).arg( pszDescription ) ); - } + QLabel *label = new QLabel( option.name ); + if ( !option.description.isEmpty() ) + label->setToolTip( QStringLiteral( "

%1

" ).arg( option.description ) ); + mOpenOptionsLayout->addRow( label, control ); } - CPLDestroyXMLNode( psDoc ); - // Set label to point to driver help page const char *pszHelpTopic = GDALGetMetadataItem( hDriver, GDAL_DMD_HELPTOPIC, nullptr ); if ( pszHelpTopic ) From 4ca1ab8fa60360717ea2502dc2719c7ddc85dd89 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 22 Jun 2024 09:05:42 +1000 Subject: [PATCH 14/19] Move VSISetPathSpecificOption logic to common function --- .../providers/gdal/qgsgdalproviderbase.cpp | 15 +--------- src/core/providers/ogr/qgsogrprovider.cpp | 13 +------- src/core/qgsgdalutils.cpp | 30 +++++++++++++++++++ src/core/qgsgdalutils.h | 12 ++++++++ .../providers/gdal/qgsgdalsourceselect.cpp | 13 +------- 5 files changed, 45 insertions(+), 38 deletions(-) diff --git a/src/core/providers/gdal/qgsgdalproviderbase.cpp b/src/core/providers/gdal/qgsgdalproviderbase.cpp index 60aba5adf1dd..10c72b80715e 100644 --- a/src/core/providers/gdal/qgsgdalproviderbase.cpp +++ b/src/core/providers/gdal/qgsgdalproviderbase.cpp @@ -26,8 +26,6 @@ #include "qgslogger.h" #include "qgsgdalproviderbase.h" #include "qgsgdalutils.h" -#include "qgssettings.h" -#include "qgsmessagelog.h" #include #include @@ -302,18 +300,7 @@ GDALDatasetH QgsGdalProviderBase::gdalOpen( const QString &uri, unsigned int nOp const QRegularExpressionMatch bucketMatch = bucketRx.match( parts.value( QStringLiteral( "path" ) ).toString() ); if ( bucketMatch.hasMatch() ) { - const QString bucket = vsiPrefix + bucketMatch.captured( 1 ); - for ( auto it = credentialOptions.constBegin(); it != credentialOptions.constEnd(); ++it ) - { -#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3, 6, 0) - VSISetPathSpecificOption( bucket.toUtf8().constData(), it.key().toUtf8().constData(), it.value().toString().toUtf8().constData() ); -#elif GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3, 5, 0) - VSISetCredential( bucket.toUtf8().constData(), it.key().toUtf8().constData(), it.value().toString().toUtf8().constData() ); -#else - ( void )bucket; - QgsMessageLog::logMessage( QObject::tr( "Cannot use VSI credential options on GDAL versions earlier than 3.5" ), QStringLiteral( "GDAL" ), Qgis::MessageLevel::Critical ); -#endif - } + QgsGdalUtils::applyVsiCredentialOptions( vsiPrefix, bucketMatch.captured( 1 ), credentialOptions ); } } diff --git a/src/core/providers/ogr/qgsogrprovider.cpp b/src/core/providers/ogr/qgsogrprovider.cpp index 0f2eea6896f1..da9f44df1216 100644 --- a/src/core/providers/ogr/qgsogrprovider.cpp +++ b/src/core/providers/ogr/qgsogrprovider.cpp @@ -441,18 +441,7 @@ QgsOgrProvider::QgsOgrProvider( QString const &uri, const ProviderOptions &optio const QRegularExpressionMatch bucketMatch = bucketRx.match( parts.value( QStringLiteral( "path" ) ).toString() ); if ( bucketMatch.hasMatch() ) { - const QString bucket = vsiPrefix + bucketMatch.captured( 1 ); - for ( auto it = credentialOptions.constBegin(); it != credentialOptions.constEnd(); ++it ) - { -#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3, 6, 0) - VSISetPathSpecificOption( bucket.toUtf8().constData(), it.key().toUtf8().constData(), it.value().toString().toUtf8().constData() ); -#elif GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3, 5, 0) - VSISetCredential( bucket.toUtf8().constData(), it.key().toUtf8().constData(), it.value().toString().toUtf8().constData() ); -#else - ( void )bucket; - QgsMessageLog::logMessage( QObject::tr( "Cannot use VSI credential options on GDAL versions earlier than 3.5" ), QStringLiteral( "GDAL" ), Qgis::MessageLevel::Critical ); -#endif - } + QgsGdalUtils::applyVsiCredentialOptions( vsiPrefix, bucketMatch.captured( 1 ), credentialOptions ); } } diff --git a/src/core/qgsgdalutils.cpp b/src/core/qgsgdalutils.cpp index f2bf33f81101..7cce59a1f1ee 100644 --- a/src/core/qgsgdalutils.cpp +++ b/src/core/qgsgdalutils.cpp @@ -19,6 +19,7 @@ #include "qgssettings.h" #include "qgscoordinatereferencesystem.h" #include "qgsrasterblock.h" +#include "qgsmessagelog.h" #define CPL_SUPRESS_CPLUSPLUS //#spellok #include "gdal.h" @@ -993,4 +994,33 @@ bool QgsGdalUtils::vrtMatchesLayerType( const QString &vrtPath, Qgis::LayerType CPLPopErrorHandler(); return static_cast< bool >( hDriver ); } + +bool QgsGdalUtils::applyVsiCredentialOptions( const QString &prefix, const QString &path, const QVariantMap &options ) +{ + QString vsiPrefix = prefix; + if ( !vsiPrefix.startsWith( '/' ) ) + vsiPrefix.prepend( '/' ); + if ( !vsiPrefix.endsWith( '/' ) ) + vsiPrefix.append( '/' ); + + QString vsiPath = path; + if ( vsiPath.endsWith( '/' ) ) + vsiPath.chop( 1 ); + + const QString bucket = vsiPrefix + vsiPath; + + for ( auto it = options.constBegin(); it != options.constEnd(); ++it ) + { +#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3, 6, 0) + VSISetPathSpecificOption( bucket.toUtf8().constData(), it.key().toUtf8().constData(), it.value().toString().toUtf8().constData() ); +#elif GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3, 5, 0) + VSISetCredential( bucket.toUtf8().constData(), it.key().toUtf8().constData(), it.value().toString().toUtf8().constData() ); +#else + ( void )bucket; + QgsMessageLog::logMessage( QObject::tr( "Cannot use VSI credential options on GDAL versions earlier than 3.5" ), QStringLiteral( "GDAL" ), Qgis::MessageLevel::Critical ); + return false; +#endif + } + return true; +} #endif diff --git a/src/core/qgsgdalutils.h b/src/core/qgsgdalutils.h index 6b587a3c177b..ebff93daec82 100644 --- a/src/core/qgsgdalutils.h +++ b/src/core/qgsgdalutils.h @@ -341,6 +341,18 @@ class CORE_EXPORT QgsGdalUtils */ static bool isVsiArchiveFileExtension( const QString &extension ); + /** + * Attempts to apply VSI credential \a options. + * + * This method uses GDAL's VSISetPathSpecificOption, which will overrwrite any existing + * options for the same VSI \a prefix and \a path. + * + * Returns TRUE if the options could be applied. + * + * \since QGIS 3.40 + */ + static bool applyVsiCredentialOptions( const QString &prefix, const QString &path, const QVariantMap &options ); + /** * Returns TRUE if the VRT file at the specified path is a VRT matching * the given layer \a type. diff --git a/src/gui/providers/gdal/qgsgdalsourceselect.cpp b/src/gui/providers/gdal/qgsgdalsourceselect.cpp index 5dc8130007e2..c20a4dfbc373 100644 --- a/src/gui/providers/gdal/qgsgdalsourceselect.cpp +++ b/src/gui/providers/gdal/qgsgdalsourceselect.cpp @@ -432,18 +432,7 @@ void QgsGdalSourceSelect::fillOpenOptions() const QRegularExpressionMatch bucketMatch = bucketRx.match( parts.value( QStringLiteral( "path" ) ).toString() ); if ( bucketMatch.hasMatch() ) { - const QString bucket = vsiPrefix + bucketMatch.captured( 1 ); - for ( auto it = credentialOptions.constBegin(); it != credentialOptions.constEnd(); ++it ) - { -#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3, 6, 0) - VSISetPathSpecificOption( bucket.toUtf8().constData(), it.key().toUtf8().constData(), it.value().toString().toUtf8().constData() ); -#elif GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3, 5, 0) - VSISetCredential( bucket.toUtf8().constData(), it.key().toUtf8().constData(), it.value().toString().toUtf8().constData() ); -#else - ( void )bucket; - QgsMessageLog::logMessage( QObject::tr( "Cannot use VSI credential options on GDAL versions earlier than 3.5" ), QStringLiteral( "GDAL" ), Qgis::MessageLevel::Critical ); -#endif - } + QgsGdalUtils::applyVsiCredentialOptions( vsiPrefix, bucketMatch.captured( 1 ), credentialOptions ); } } From 3f978dd055306fdd6c0aa53cdc2e3b0fa607cd1c Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 22 Jun 2024 10:52:08 +1000 Subject: [PATCH 15/19] Add credential options to OGR source select --- src/gui/providers/ogr/qgsogrsourceselect.cpp | 69 ++++++++++-- src/gui/providers/ogr/qgsogrsourceselect.h | 8 +- src/ui/qgsogrsourceselectbase.ui | 105 +++++++++---------- 3 files changed, 120 insertions(+), 62 deletions(-) diff --git a/src/gui/providers/ogr/qgsogrsourceselect.cpp b/src/gui/providers/ogr/qgsogrsourceselect.cpp index 2aa0b6632b8f..a554d0dcce18 100644 --- a/src/gui/providers/ogr/qgsogrsourceselect.cpp +++ b/src/gui/providers/ogr/qgsogrsourceselect.cpp @@ -24,7 +24,6 @@ #include #include -#include "qgsapplication.h" #include "qgslogger.h" #include "qgsvectordataprovider.h" #include "qgssettings.h" @@ -35,6 +34,8 @@ #include "qgsgdalutils.h" #include "qgsspinbox.h" #include "qgsdoublespinbox.h" +#include "qgsgdalcredentialoptionswidget.h" +#include "qgshelp.h" #include #include @@ -108,8 +109,6 @@ QgsOgrSourceSelect::QgsOgrSourceSelect( QWidget *parent, Qt::WindowFlags fl, Qgs cmbDatabaseTypes->blockSignals( false ); cmbConnections->blockSignals( false ); - mAuthWarning->setText( tr( " Additional credential options are required as documented here." ).arg( QLatin1String( "http://gdal.org/gdal_virtual_file_systems.html#gdal_virtual_file_systems_network" ) ) ); - mFileWidget->setDialogTitle( tr( "Open OGR Supported Vector Dataset(s)" ) ); mVectorFileFilter = QgsProviderRegistry::instance()->fileVectorFilters(); mFileWidget->setFilter( mVectorFileFilter ); @@ -146,6 +145,12 @@ QgsOgrSourceSelect::QgsOgrSourceSelect( QWidget *parent, Qt::WindowFlags fl, Qgs mAuthSettingsProtocol->setDataprovider( QStringLiteral( "ogr" ) ); mOpenOptionsGroupBox->setVisible( false ); + + mCredentialsWidget = new QgsGdalCredentialOptionsWidget(); + mCredentialOptionsLayout->addWidget( mCredentialsWidget ); + mCredentialOptionsGroupBox->setVisible( false ); + + connect( mCredentialsWidget, &QgsGdalCredentialOptionsWidget::optionsChanged, this, &QgsOgrSourceSelect::credentialOptionsChanged ); } QStringList QgsOgrSourceSelect::dataSources() @@ -295,7 +300,6 @@ void QgsOgrSourceSelect::setProtocolWidgetsVisibility() mBucket->show(); labelKey->show(); mKey->show(); - mAuthWarning->show(); } else { @@ -306,7 +310,6 @@ void QgsOgrSourceSelect::setProtocolWidgetsVisibility() mBucket->hide(); labelKey->hide(); mKey->hide(); - mAuthWarning->hide(); } } @@ -347,6 +350,8 @@ void QgsOgrSourceSelect::computeDataSources( bool interactive ) } } + const QVariantMap credentialOptions = !mCredentialOptionsGroupBox->isHidden() ? mCredentialOptions : QVariantMap(); + if ( radioSrcDatabase->isChecked() ) { if ( !settings.contains( '/' + cmbDatabaseTypes->currentText() @@ -441,6 +446,8 @@ void QgsOgrSourceSelect::computeDataSources( bool interactive ) QVariantMap parts; if ( !openOptions.isEmpty() ) parts.insert( QStringLiteral( "openOptions" ), openOptions ); + if ( !credentialOptions.isEmpty() ) + parts.insert( QStringLiteral( "credentialOptions" ), credentialOptions ); parts.insert( QStringLiteral( "path" ), QgsGdalGuiUtils::createProtocolURI( cmbProtocolTypes->currentData().toString(), uri, @@ -529,6 +536,7 @@ void QgsOgrSourceSelect::radioSrcFile_toggled( bool checked ) dbGroupBox->hide(); protocolGroupBox->hide(); clearOpenOptions(); + updateProtocolOptions(); mFileWidget->setDialogTitle( tr( "Open an OGR Supported Vector Layer" ) ); mFileWidget->setFilter( mVectorFileFilter ); @@ -551,6 +559,7 @@ void QgsOgrSourceSelect::radioSrcOgcApi_toggled( bool checked ) mVectorPath = mFileWidget->filePath(); emit enableButtons( ! mVectorPath.isEmpty() ); fillOpenOptions(); + updateProtocolOptions(); } else { @@ -568,6 +577,7 @@ void QgsOgrSourceSelect::radioSrcDirectory_toggled( bool checked ) dbGroupBox->hide(); protocolGroupBox->hide(); clearOpenOptions(); + updateProtocolOptions(); mFileWidget->setDialogTitle( tr( "Open Directory" ) ); mFileWidget->setStorageMode( QgsFileWidget::GetDirectory ); @@ -589,6 +599,7 @@ void QgsOgrSourceSelect::radioSrcDatabase_toggled( bool checked ) dbGroupBox->show(); layout()->blockSignals( false ); clearOpenOptions(); + updateProtocolOptions(); setConnectionTypeListPosition(); populateConnectionList(); @@ -607,6 +618,7 @@ void QgsOgrSourceSelect::radioSrcProtocol_toggled( bool checked ) dbGroupBox->hide(); protocolGroupBox->show(); clearOpenOptions(); + updateProtocolOptions(); mDataSourceType = QStringLiteral( "protocol" ); @@ -651,6 +663,8 @@ void QgsOgrSourceSelect::cmbProtocolTypes_currentIndexChanged( const QString &te { Q_UNUSED( text ) setProtocolWidgetsVisibility(); + clearOpenOptions(); + updateProtocolOptions(); } //********************end auto connected slots *****************/ @@ -718,6 +732,30 @@ bool QgsOgrSourceSelect::configureFromUri( const QString &uri ) return true; } +void QgsOgrSourceSelect::updateProtocolOptions() +{ + const QString currentProtocol = cmbProtocolTypes->currentData().toString(); + if ( radioSrcProtocol->isChecked() && QgsGdalGuiUtils::isProtocolCloudType( currentProtocol ) ) + { + mCredentialsWidget->setDriver( currentProtocol ); + mCredentialOptionsGroupBox->setVisible( true ); + } + else + { + mCredentialOptionsGroupBox->setVisible( false ); + } +} + +void QgsOgrSourceSelect::credentialOptionsChanged() +{ + const QVariantMap newCredentialOptions = mCredentialsWidget->credentialOptions(); + if ( newCredentialOptions == mCredentialOptions ) + return; + + mCredentialOptions = newCredentialOptions; + fillOpenOptions(); +} + void QgsOgrSourceSelect::clearOpenOptions() { mOpenOptionsWidgets.clear(); @@ -738,11 +776,28 @@ void QgsOgrSourceSelect::fillOpenOptions() if ( mDataSources.isEmpty() ) return; + const QString firstDataSource = mDataSources.at( 0 ); + QVariantMap parts = QgsProviderRegistry::instance()->decodeUri( QStringLiteral( "ogr" ), firstDataSource ); + const QVariantMap credentialOptions = parts.value( QStringLiteral( "credentialOptions" ) ).toMap(); + const QString vsiPrefix = QgsGdalUtils::vsiPrefixForPath( firstDataSource ); + parts.remove( QStringLiteral( "credentialOptions" ) ); + if ( !credentialOptions.isEmpty() && !vsiPrefix.isEmpty() ) + { + const thread_local QRegularExpression bucketRx( QStringLiteral( "^(.*?)/" ) ); + const QRegularExpressionMatch bucketMatch = bucketRx.match( parts.value( QStringLiteral( "path" ) ).toString() ); + if ( bucketMatch.hasMatch() ) + { + QgsGdalUtils::applyVsiCredentialOptions( vsiPrefix, bucketMatch.captured( 1 ), credentialOptions ); + } + } + + const QString ogrUri = QgsProviderRegistry::instance()->encodeUri( QStringLiteral( "ogr" ), parts ); + GDALDriverH hDriver; - if ( STARTS_WITH_CI( mDataSources[0].toUtf8().toStdString().c_str(), "PG:" ) ) + if ( STARTS_WITH_CI( ogrUri.toUtf8().toStdString().c_str(), "PG:" ) ) hDriver = GDALGetDriverByName( "PostgreSQL" ); // otherwise the PostgisRaster driver gets identified else - hDriver = GDALIdentifyDriverEx( mDataSources[0].toUtf8().toStdString().c_str(), GDAL_OF_VECTOR, nullptr, nullptr ); + hDriver = GDALIdentifyDriverEx( ogrUri.toUtf8().toStdString().c_str(), GDAL_OF_VECTOR, nullptr, nullptr ); if ( hDriver == nullptr ) return; diff --git a/src/gui/providers/ogr/qgsogrsourceselect.h b/src/gui/providers/ogr/qgsogrsourceselect.h index ed3f5722861e..8820984ada84 100644 --- a/src/gui/providers/ogr/qgsogrsourceselect.h +++ b/src/gui/providers/ogr/qgsogrsourceselect.h @@ -31,7 +31,6 @@ #include #include "ui_qgsogrsourceselectbase.h" -#include "qgshelp.h" #include "qgsproviderregistry.h" #include "qgsabstractdatasourcewidget.h" #include "qgis_gui.h" @@ -40,6 +39,8 @@ ///@cond PRIVATE #define SIP_NO_FILE +class QgsGdalCredentialOptionsWidget; + /** * Class for a dialog to select the type and source for ogr vectors, supports * file, database, directory and protocol sources. @@ -110,6 +111,8 @@ class QgsOgrSourceSelect : public QgsAbstractDataSourceWidget, private Ui::QgsOg void cmbProtocolTypes_currentIndexChanged( const QString &text ); void showHelp(); bool configureFromUri( const QString &uri ) override; + void updateProtocolOptions(); + void credentialOptionsChanged(); private: @@ -117,10 +120,11 @@ class QgsOgrSourceSelect : public QgsAbstractDataSourceWidget, private Ui::QgsOg void clearOpenOptions(); void fillOpenOptions(); std::vector mOpenOptionsWidgets; + QgsGdalCredentialOptionsWidget *mCredentialsWidget = nullptr; bool mIsOgcApi = false; + QVariantMap mCredentialOptions; QString mVectorPath; - }; ///@endcond diff --git a/src/ui/qgsogrsourceselectbase.ui b/src/ui/qgsogrsourceselectbase.ui index 96c39ca930ba..ebc7de58a5f5 100644 --- a/src/ui/qgsogrsourceselectbase.ui +++ b/src/ui/qgsogrsourceselectbase.ui @@ -6,7 +6,7 @@ 0 0 - 522 + 545 786
@@ -141,23 +141,19 @@ Protocol + + + - - - - Type - - - - - + + - &URI + Object key - protocolURI + mKey @@ -177,33 +173,7 @@ - - - - Object key - - - mKey - - - - - - - - - - - - true - - - true - - - - Authentication @@ -234,6 +204,23 @@ + + + + Type + + + + + + + &URI + + + protocolURI + + + @@ -365,6 +352,18 @@ + + + + Credential Options + + + + + + + + @@ -390,8 +389,8 @@ 0 0 - 506 - 78 + 529 + 77 @@ -408,8 +407,8 @@ 0 - - + + Options @@ -461,27 +460,27 @@ - QgsFileWidget - QWidget -
qgsfilewidget.h
+ QgsCollapsibleGroupBox + QGroupBox +
qgscollapsiblegroupbox.h
1
- QgsAuthSettingsWidget - QWidget -
qgsauthsettingswidget.h
+ QgsScrollArea + QScrollArea +
qgsscrollarea.h
1
- QgsCollapsibleGroupBox + QgsFileWidget QWidget -
qgscollapsiblegroupbox.h
+
qgsfilewidget.h
1
- QgsScrollArea - QScrollArea -
qgsscrollarea.h
+ QgsAuthSettingsWidget + QWidget +
qgsauthsettingswidget.h
1
From c5c4b5bdaa2d074f67d6b50e1539c1f6e8e19c38 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 22 Jun 2024 10:56:59 +1000 Subject: [PATCH 16/19] Sort protocols alphabetically in combo --- src/gui/providers/gdal/qgsgdalsourceselect.cpp | 8 ++++++-- src/gui/providers/ogr/qgsogrsourceselect.cpp | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/gui/providers/gdal/qgsgdalsourceselect.cpp b/src/gui/providers/gdal/qgsgdalsourceselect.cpp index c20a4dfbc373..c5ba79a21d80 100644 --- a/src/gui/providers/gdal/qgsgdalsourceselect.cpp +++ b/src/gui/providers/gdal/qgsgdalsourceselect.cpp @@ -48,8 +48,12 @@ QgsGdalSourceSelect::QgsGdalSourceSelect( QWidget *parent, Qt::WindowFlags fl, Q whileBlocking( radioSrcFile )->setChecked( true ); protocolGroupBox->hide(); - const QList< QgsGdalUtils::VsiNetworkFileSystemDetails > vsiDetails = QgsGdalUtils::vsiNetworkFileSystems(); - for ( const QgsGdalUtils::VsiNetworkFileSystemDetails &vsiDetail : vsiDetails ) + QList< QgsGdalUtils::VsiNetworkFileSystemDetails > vsiDetails = QgsGdalUtils::vsiNetworkFileSystems(); + std::sort( vsiDetails.begin(), vsiDetails.end(), []( const QgsGdalUtils::VsiNetworkFileSystemDetails & a, const QgsGdalUtils::VsiNetworkFileSystemDetails & b ) + { + return QString::localeAwareCompare( a.name, b.name ) < 0; + } ); + for ( const QgsGdalUtils::VsiNetworkFileSystemDetails &vsiDetail : std::as_const( vsiDetails ) ) { cmbProtocolTypes->addItem( vsiDetail.name, vsiDetail.identifier ); } diff --git a/src/gui/providers/ogr/qgsogrsourceselect.cpp b/src/gui/providers/ogr/qgsogrsourceselect.cpp index a554d0dcce18..dd65ade36eb9 100644 --- a/src/gui/providers/ogr/qgsogrsourceselect.cpp +++ b/src/gui/providers/ogr/qgsogrsourceselect.cpp @@ -99,8 +99,12 @@ QgsOgrSourceSelect::QgsOgrSourceSelect( QWidget *parent, Qt::WindowFlags fl, Qgs } //add protocol drivers - const QList< QgsGdalUtils::VsiNetworkFileSystemDetails > vsiDetails = QgsGdalUtils::vsiNetworkFileSystems(); - for ( const QgsGdalUtils::VsiNetworkFileSystemDetails &vsiDetail : vsiDetails ) + QList< QgsGdalUtils::VsiNetworkFileSystemDetails > vsiDetails = QgsGdalUtils::vsiNetworkFileSystems(); + std::sort( vsiDetails.begin(), vsiDetails.end(), []( const QgsGdalUtils::VsiNetworkFileSystemDetails & a, const QgsGdalUtils::VsiNetworkFileSystemDetails & b ) + { + return QString::localeAwareCompare( a.name, b.name ) < 0; + } ); + for ( const QgsGdalUtils::VsiNetworkFileSystemDetails &vsiDetail : std::as_const( vsiDetails ) ) { cmbProtocolTypes->addItem( vsiDetail.name, vsiDetail.identifier ); } From 1e95e7a58a94aad7d2c50548293411e6a6228215 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 22 Jun 2024 11:13:59 +1000 Subject: [PATCH 17/19] Move isProtocolCloudType to core utils class --- src/core/qgsgdalutils.cpp | 17 +++++++++++++++++ src/core/qgsgdalutils.h | 7 +++++++ src/gui/ogr/qgsogrhelperfunctions.cpp | 11 ----------- src/gui/ogr/qgsogrhelperfunctions.h | 5 ----- .../gdal/qgsgdalcredentialoptionswidget.cpp | 2 +- src/gui/providers/gdal/qgsgdalsourceselect.cpp | 6 +++--- src/gui/providers/ogr/qgsogrsourceselect.cpp | 6 +++--- 7 files changed, 31 insertions(+), 23 deletions(-) diff --git a/src/core/qgsgdalutils.cpp b/src/core/qgsgdalutils.cpp index 7cce59a1f1ee..f1fbdc672fa0 100644 --- a/src/core/qgsgdalutils.cpp +++ b/src/core/qgsgdalutils.cpp @@ -965,6 +965,23 @@ bool QgsGdalUtils::isVsiArchiveFileExtension( const QString &extension ) return vsiArchiveFileExtensions().contains( extWithDot.toLower() ); } +bool QgsGdalUtils::isProtocolCloudType( const QString &protocol ) +{ + QString vsiPrefix = protocol; + if ( vsiPrefix.startsWith( '/' ) ) + vsiPrefix = vsiPrefix.mid( 1 ); + if ( vsiPrefix.endsWith( '/' ) ) + vsiPrefix.chop( 1 ); + + return ( vsiPrefix == QLatin1String( "vsis3" ) || + vsiPrefix == QLatin1String( "vsigs" ) || + vsiPrefix == QLatin1String( "vsiaz" ) || + vsiPrefix == QLatin1String( "vsiadls" ) || + vsiPrefix == QLatin1String( "vsioss" ) || + vsiPrefix == QLatin1String( "vsiswift" ) || + vsiPrefix == QLatin1String( "vsihdfs" ) ); +} + bool QgsGdalUtils::vrtMatchesLayerType( const QString &vrtPath, Qgis::LayerType type ) { CPLPushErrorHandler( CPLQuietErrorHandler ); diff --git a/src/core/qgsgdalutils.h b/src/core/qgsgdalutils.h index ebff93daec82..0dc04440b97a 100644 --- a/src/core/qgsgdalutils.h +++ b/src/core/qgsgdalutils.h @@ -341,6 +341,13 @@ class CORE_EXPORT QgsGdalUtils */ static bool isVsiArchiveFileExtension( const QString &extension ); + /** + * Returns TRUE if \a protocol (eg "vsis3") is considered a cloud type. + * + * \since QGIS 3.40 + */ + static bool isProtocolCloudType( const QString &protocol ); + /** * Attempts to apply VSI credential \a options. * diff --git a/src/gui/ogr/qgsogrhelperfunctions.cpp b/src/gui/ogr/qgsogrhelperfunctions.cpp index c52528873c2b..d49d0886b48b 100644 --- a/src/gui/ogr/qgsogrhelperfunctions.cpp +++ b/src/gui/ogr/qgsogrhelperfunctions.cpp @@ -290,17 +290,6 @@ QString QgsGdalGuiUtils::createProtocolURI( const QString &type, const QString & return uri; } -bool QgsGdalGuiUtils::isProtocolCloudType( const QString &protocol ) -{ - return ( protocol == QLatin1String( "vsis3" ) || - protocol == QLatin1String( "vsigs" ) || - protocol == QLatin1String( "vsiaz" ) || - protocol == QLatin1String( "vsiadls" ) || - protocol == QLatin1String( "vsioss" ) || - protocol == QLatin1String( "vsiswift" ) || - protocol == QLatin1String( "vsihdfs" ) ); -} - QWidget *QgsGdalGuiUtils::createWidgetForOption( const QgsGdalOption &option, QWidget *parent, bool includeDefaultChoices ) { switch ( option.type ) diff --git a/src/gui/ogr/qgsogrhelperfunctions.h b/src/gui/ogr/qgsogrhelperfunctions.h index ecea077a997a..f235c6b80d5d 100644 --- a/src/gui/ogr/qgsogrhelperfunctions.h +++ b/src/gui/ogr/qgsogrhelperfunctions.h @@ -48,11 +48,6 @@ class GUI_EXPORT QgsGdalGuiUtils */ static QString createProtocolURI( const QString &type, const QString &url, const QString &configId, const QString &username, const QString &password, bool expandAuthConfig = false ); - /** - * Returns TRUE if \a protocol (eg "vsis3") is considered a cloud type. - */ - static bool isProtocolCloudType( const QString &protocol ); - /** * Creates a new widget for configuration a GDAL \a option. * diff --git a/src/gui/providers/gdal/qgsgdalcredentialoptionswidget.cpp b/src/gui/providers/gdal/qgsgdalcredentialoptionswidget.cpp index 88e4c8e4b537..6c2a2494505b 100644 --- a/src/gui/providers/gdal/qgsgdalcredentialoptionswidget.cpp +++ b/src/gui/providers/gdal/qgsgdalcredentialoptionswidget.cpp @@ -547,7 +547,7 @@ void QgsGdalCredentialOptionsWidget::setDriver( const QString &driver ) mDriver = driver; - if ( !QgsGdalGuiUtils::isProtocolCloudType( mDriver ) ) + if ( !QgsGdalUtils::isProtocolCloudType( mDriver ) ) { mModel->setAvailableOptions( {} ); return; diff --git a/src/gui/providers/gdal/qgsgdalsourceselect.cpp b/src/gui/providers/gdal/qgsgdalsourceselect.cpp index c5ba79a21d80..dd2d531024a4 100644 --- a/src/gui/providers/gdal/qgsgdalsourceselect.cpp +++ b/src/gui/providers/gdal/qgsgdalsourceselect.cpp @@ -105,7 +105,7 @@ QgsGdalSourceSelect::QgsGdalSourceSelect( QWidget *parent, Qt::WindowFlags fl, Q void QgsGdalSourceSelect::setProtocolWidgetsVisibility() { - if ( QgsGdalGuiUtils::isProtocolCloudType( cmbProtocolTypes->currentData().toString() ) ) + if ( QgsGdalUtils::isProtocolCloudType( cmbProtocolTypes->currentData().toString() ) ) { labelProtocolURI->hide(); protocolURI->hide(); @@ -360,7 +360,7 @@ void QgsGdalSourceSelect::computeDataSources() } else if ( radioSrcProtocol->isChecked() ) { - bool cloudType = QgsGdalGuiUtils::isProtocolCloudType( cmbProtocolTypes->currentData().toString() ); + bool cloudType = QgsGdalUtils::isProtocolCloudType( cmbProtocolTypes->currentData().toString() ); if ( !cloudType && protocolURI->text().isEmpty() ) { return; @@ -524,7 +524,7 @@ void QgsGdalSourceSelect::showHelp() void QgsGdalSourceSelect::updateProtocolOptions() { const QString currentProtocol = cmbProtocolTypes->currentData().toString(); - if ( radioSrcProtocol->isChecked() && QgsGdalGuiUtils::isProtocolCloudType( currentProtocol ) ) + if ( radioSrcProtocol->isChecked() && QgsGdalUtils::isProtocolCloudType( currentProtocol ) ) { mCredentialsWidget->setDriver( currentProtocol ); mCredentialOptionsGroupBox->setVisible( true ); diff --git a/src/gui/providers/ogr/qgsogrsourceselect.cpp b/src/gui/providers/ogr/qgsogrsourceselect.cpp index dd65ade36eb9..cfb19c593866 100644 --- a/src/gui/providers/ogr/qgsogrsourceselect.cpp +++ b/src/gui/providers/ogr/qgsogrsourceselect.cpp @@ -295,7 +295,7 @@ void QgsOgrSourceSelect::setSelectedConnection() void QgsOgrSourceSelect::setProtocolWidgetsVisibility() { - if ( QgsGdalGuiUtils::isProtocolCloudType( cmbProtocolTypes->currentData().toString() ) ) + if ( QgsGdalUtils::isProtocolCloudType( cmbProtocolTypes->currentData().toString() ) ) { labelProtocolURI->hide(); protocolURI->hide(); @@ -415,7 +415,7 @@ void QgsOgrSourceSelect::computeDataSources( bool interactive ) } else if ( radioSrcProtocol->isChecked() ) { - bool cloudType = QgsGdalGuiUtils::isProtocolCloudType( cmbProtocolTypes->currentData().toString() ); + bool cloudType = QgsGdalUtils::isProtocolCloudType( cmbProtocolTypes->currentData().toString() ); if ( !cloudType && protocolURI->text().isEmpty() ) { if ( interactive ) @@ -739,7 +739,7 @@ bool QgsOgrSourceSelect::configureFromUri( const QString &uri ) void QgsOgrSourceSelect::updateProtocolOptions() { const QString currentProtocol = cmbProtocolTypes->currentData().toString(); - if ( radioSrcProtocol->isChecked() && QgsGdalGuiUtils::isProtocolCloudType( currentProtocol ) ) + if ( radioSrcProtocol->isChecked() && QgsGdalUtils::isProtocolCloudType( currentProtocol ) ) { mCredentialsWidget->setDriver( currentProtocol ); mCredentialOptionsGroupBox->setVisible( true ); From 995a9ae9a2e91ad42a3c28e5e20b28b5f47e2654 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 22 Jun 2024 11:16:53 +1000 Subject: [PATCH 18/19] [ogr] Handle VSI credentials in querySublayers --- .../providers/ogr/qgsogrprovidermetadata.cpp | 20 +++++++-- tests/src/core/testqgsogrprovider.cpp | 44 ++++++++++++++++--- 2 files changed, 54 insertions(+), 10 deletions(-) diff --git a/src/core/providers/ogr/qgsogrprovidermetadata.cpp b/src/core/providers/ogr/qgsogrprovidermetadata.cpp index 29e155e2ecee..d843002971c5 100644 --- a/src/core/providers/ogr/qgsogrprovidermetadata.cpp +++ b/src/core/providers/ogr/qgsogrprovidermetadata.cpp @@ -799,7 +799,8 @@ QList QgsOgrProviderMetadata::querySublayers( const } if ( !uriParts.value( QStringLiteral( "vsiPrefix" ) ).toString().isEmpty() - && uriParts.value( QStringLiteral( "vsiSuffix" ) ).toString().isEmpty() ) + && uriParts.value( QStringLiteral( "vsiSuffix" ) ).toString().isEmpty() + && QgsGdalUtils::isVsiArchivePrefix( uriParts.value( QStringLiteral( "vsiPrefix" ) ).toString() ) ) { // get list of files inside archive file QgsDebugMsgLevel( QStringLiteral( "Open file %1 with gdal vsi" ).arg( vsiPrefix + uriParts.value( QStringLiteral( "path" ) ).toString() ), 3 ); @@ -852,6 +853,7 @@ QList QgsOgrProviderMetadata::querySublayers( const ? pathInfo.suffix().toLower() : QFileInfo( uriParts.value( QStringLiteral( "vsiSuffix" ) ).toString() ).suffix().toLower(); bool isOgrSupportedDirectory = pathInfo.isDir() && dirExtensions.contains( suffix ); + const bool isVsiNetworkProtocol = QgsGdalUtils::isProtocolCloudType( uriParts.value( QStringLiteral( "vsiPrefix" ) ).toString() ); bool forceDeepScanDir = false; if ( pathInfo.isDir() && !isOgrSupportedDirectory ) @@ -860,13 +862,13 @@ QList QgsOgrProviderMetadata::querySublayers( const forceDeepScanDir = it.hasNext(); } - if ( ( flags & Qgis::SublayerQueryFlag::FastScan ) && ( pathInfo.isFile() || pathInfo.isDir() ) && !forceDeepScanDir ) + if ( ( flags & Qgis::SublayerQueryFlag::FastScan ) && ( pathInfo.isFile() || pathInfo.isDir() || isVsiNetworkProtocol ) && !forceDeepScanDir ) { // fast scan, so we don't actually try to open the dataset and instead just check the extension alone const QStringList fileExtensions = QgsOgrProviderUtils::fileExtensions(); // allow only normal files or supported directories to continue - if ( !isOgrSupportedDirectory && !pathInfo.isFile() ) + if ( !isOgrSupportedDirectory && !pathInfo.isFile() && !isVsiNetworkProtocol ) return {}; if ( !fileExtensions.contains( suffix ) && !dirExtensions.contains( suffix ) ) @@ -949,6 +951,18 @@ QList QgsOgrProviderMetadata::querySublayers( const firstLayerUriParts.insert( QStringLiteral( "vsiPrefix" ), uriParts.value( QStringLiteral( "vsiPrefix" ) ) ); if ( !uriParts.value( QStringLiteral( "vsiSuffix" ) ).toString().isEmpty() ) firstLayerUriParts.insert( QStringLiteral( "vsiSuffix" ), uriParts.value( QStringLiteral( "vsiSuffix" ) ) ); + + const QVariantMap credentialOptions = uriParts.value( QStringLiteral( "credentialOptions" ) ).toMap(); + if ( !credentialOptions.isEmpty() && !uriParts.value( QStringLiteral( "vsiPrefix" ) ).toString().isEmpty() ) + { + const thread_local QRegularExpression bucketRx( QStringLiteral( "^(.*?)/" ) ); + const QRegularExpressionMatch bucketMatch = bucketRx.match( uriParts.value( QStringLiteral( "path" ) ).toString() ); + if ( bucketMatch.hasMatch() ) + { + QgsGdalUtils::applyVsiCredentialOptions( uriParts.value( QStringLiteral( "vsiPrefix" ) ).toString(), bucketMatch.captured( 1 ), credentialOptions ); + } + } + firstLayerUriParts.insert( QStringLiteral( "path" ), uriParts.value( QStringLiteral( "path" ) ) ); CPLPushErrorHandler( CPLQuietErrorHandler ); diff --git a/tests/src/core/testqgsogrprovider.cpp b/tests/src/core/testqgsogrprovider.cpp index e70ff2637be2..f1150b54475d 100644 --- a/tests/src/core/testqgsogrprovider.cpp +++ b/tests/src/core/testqgsogrprovider.cpp @@ -18,13 +18,14 @@ #include "qgstest.h" //qgis includes... -#include -#include -#include -#include -#include -#include -#include +#include "qgis.h" +#include "qgssettings.h" +#include "qgsapplication.h" +#include "qgsproviderregistry.h" +#include "qgsvectorlayer.h" +#include "qgsnetworkaccessmanager.h" +#include "qgsprovidermetadata.h" +#include "qgsprovidersublayerdetails.h" #include #include @@ -55,6 +56,7 @@ class TestQgsOgrProvider : public QgsTest void absoluteRelativeUri(); void testExtent(); void testVsiCredentialOptions(); + void testVsiCredentialOptionsQuerySublayers(); private: QString mTestDataDir; @@ -515,6 +517,34 @@ void TestQgsOgrProvider::testVsiCredentialOptions() #endif } +void TestQgsOgrProvider::testVsiCredentialOptionsQuerySublayers() +{ +#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3, 6, 0) + QgsProviderMetadata *ogrMetadata = QgsProviderRegistry::instance()->providerMetadata( "ogr" ); + QVERIFY( ogrMetadata ); + + // test that credential options are correctly handled when querying sublayers + QList< QgsProviderSublayerDetails> subLayers = ogrMetadata->querySublayers( QStringLiteral( "/vsis3/sublayerstestbucket/test.shp|credential:AWS_NO_SIGN_REQUEST=YES|credential:AWS_REGION=eu-central-3|credential:AWS_S3_ENDPOINT=localhost" ) ); + // ideally we'd test with a real dataset here! + QVERIFY( subLayers.isEmpty() ); + + // confirm that GDAL VSI configuration options are set + QString noSign( VSIGetPathSpecificOption( "/vsis3/sublayerstestbucket", "AWS_NO_SIGN_REQUEST", nullptr ) ); + QCOMPARE( noSign, QStringLiteral( "YES" ) ); + QString region( VSIGetPathSpecificOption( "/vsis3/sublayerstestbucket", "AWS_REGION", nullptr ) ); + QCOMPARE( region, QStringLiteral( "eu-central-3" ) ); + + subLayers = ogrMetadata->querySublayers( QStringLiteral( "/vsis3/sublayerstestbucket/test.shp|credential:AWS_NO_SIGN_REQUEST=YES|credential:AWS_REGION=eu-central-3|credential:AWS_S3_ENDPOINT=localhost" ), Qgis::SublayerQueryFlag::FastScan ); + // ideally we'd test with a real dataset here! + QCOMPARE( subLayers.size(), 1 ); + QCOMPARE( subLayers.at( 0 ).name(), QStringLiteral( "test" ) ); + QCOMPARE( subLayers.at( 0 ).uri(), QStringLiteral( "/vsis3/sublayerstestbucket/test.shp|credential:AWS_NO_SIGN_REQUEST=YES|credential:AWS_REGION=eu-central-3|credential:AWS_S3_ENDPOINT=localhost" ) ); + QCOMPARE( subLayers.at( 0 ).providerKey(), QStringLiteral( "ogr" ) ); + QCOMPARE( subLayers.at( 0 ).type(), Qgis::LayerType::Vector ); + +#endif +} + QGSTEST_MAIN( TestQgsOgrProvider ) #include "testqgsogrprovider.moc" From 2c00e70e2fae40aeb47b4fa1cf0f00c358d4dfe3 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 22 Jun 2024 11:23:51 +1000 Subject: [PATCH 19/19] Fix window title check --- src/ui/qgsgdalcredentialoptionswidgetbase.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/qgsgdalcredentialoptionswidgetbase.ui b/src/ui/qgsgdalcredentialoptionswidgetbase.ui index 41b50fbca1bb..23de95572e76 100644 --- a/src/ui/qgsgdalcredentialoptionswidgetbase.ui +++ b/src/ui/qgsgdalcredentialoptionswidgetbase.ui @@ -11,7 +11,7 @@
- Form + Credentials