diff --git a/python/core/auto_generated/metadata/qgsabstractlayermetadataprovider.sip.in b/python/core/auto_generated/metadata/qgsabstractlayermetadataprovider.sip.in new file mode 100644 index 00000000000..2cb2a4fc4c7 --- /dev/null +++ b/python/core/auto_generated/metadata/qgsabstractlayermetadataprovider.sip.in @@ -0,0 +1,201 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/metadata/qgsabstractlayermetadataprovider.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + + +struct QgsMetadataSearchContext +{ + QgsCoordinateTransformContext transformContext; +}; + +class QgsLayerMetadataProviderResult: QgsLayerMetadata +{ +%Docstring(signature="appended") +Result record of layer metadata provider search. +The result contains QGIS metadata information and all information +that is required by QGIS to load the layer and to filter +the results. + +The class extends :py:class:`QgsLayerMetadata` by adding information +taken directly from the provider which is required for +filtering (geographic extent) or because the actual +values may be different by those stored in the metadata +(CRS authid) or totally missing from the metadata +(data provider name and layer type). + +.. versionadded:: 3.28 +%End + +%TypeHeaderCode +#include "qgsabstractlayermetadataprovider.h" +%End + public: + + QgsLayerMetadataProviderResult( const QgsLayerMetadata &metadata ); +%Docstring +Constructor for QgsLayerMetadataProviderResult. + +:param metadata: layer metadata. +%End + + const QgsPolygon &geographicExtent() const; +%Docstring +Returns the layer extent in EPSG:4326 +%End + + void setGeographicExtent( const QgsPolygon &geographicExtent ); +%Docstring +Sets the layer extent in EPSG:4326 to ``geographicExtent`` +%End + + const QgsWkbTypes::GeometryType &geometryType() const; +%Docstring +Returns the layer geometry type. +%End + + void setGeometryType( const QgsWkbTypes::GeometryType &geometryType ); +%Docstring +Sets the layer geometry type to ``geometryType``. +%End + + const QString &authid() const; +%Docstring +Returns the layer CRS authid. +%End + + void setAuthid( const QString &authid ); +%Docstring +Sets the layer ``authid``. +%End + + const QString &uri() const; +%Docstring +Returns the layer data source URI. +%End + + void setUri( const QString &Uri ); +%Docstring +Sets the layer data source URI to ``Uri``. +%End + + const QString &dataProviderName() const; +%Docstring +Returns the data provider name. +%End + + void setDataProviderName( const QString &dataProviderName ); +%Docstring +Sets the data provider name to ``dataProviderName``. +%End + + QgsMapLayerType layerType() const; +%Docstring +Returns the layer type. +%End + + void setLayerType( QgsMapLayerType layerType ); +%Docstring +Sets the layer type to ``layerType``. +%End + + const QString &standardUri() const; +%Docstring +Returns the metadata standard URI (usually "http://mrcc.com/qgis.dtd") +%End + + void setStandardUri( const QString &standardUri ); +%Docstring +Sets the metadata standard URI to ``standardUri``. +%End + +}; + +class QgsLayerMetadataSearchResults +{ +%Docstring(signature="appended") +Container of result records from a layer metadata search. + +Contains the records of the layer metadata provider that matched the +search criteria and the list of the errors that occurred while +searching for metadata. + +.. versionadded:: 3.28 +%End + +%TypeHeaderCode +#include "qgsabstractlayermetadataprovider.h" +%End + public: + + QList metadata() const; +%Docstring +Returns the list of metadata results. +%End + + void addMetadata( const QgsLayerMetadataProviderResult &metadata ); +%Docstring +Adds a ``Metadata`` record to the list of results. +%End + + QStringList errors() const; +%Docstring +Returns the list of errors occurred during a metadata search. +%End + + void addError( const QString &error ); +%Docstring +Adds a ``error`` to the list of errors. +%End + +}; + +class QgsAbstractLayerMetadataProvider +{ +%Docstring(signature="appended") +Layer metadata provider backend interface. + +.. versionadded:: 3.28 +%End + +%TypeHeaderCode +#include "qgsabstractlayermetadataprovider.h" +%End + public: + + virtual QString id() const = 0; +%Docstring +Returns the id of the layer metadata provider implementation, usually the name of the data provider +but it may be another unique identifier. +%End + + virtual QgsLayerMetadataSearchResults search( const QgsMetadataSearchContext &searchContext, const QString &searchString = QString(), const QgsRectangle &geographicExtent = QgsRectangle(), QgsFeedback *feedback = 0 ) const = 0; +%Docstring +Searches for metadata optionally filtering by search string and geographic extent. + +:param searchContext: context for the metadata search. +:param searchString: defines a filter to limit the results to the records where the search string appears in the "identifier", "title" or "abstract" metadata fields, a case-insensitive comparison is used for the match. +:param geographicExtent: defines a filter where the spatial extent matches the given extent in EPSG:4326 +:param feedback: can be used to monitor and control the search process. + +:return: a :py:class:`QgsLayerMetadataSearchResult` object with a list of metadata and errors +%End + + virtual ~QgsAbstractLayerMetadataProvider(); + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/metadata/qgsabstractlayermetadataprovider.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/core/auto_generated/metadata/qgslayermetadataproviderregistry.sip.in b/python/core/auto_generated/metadata/qgslayermetadataproviderregistry.sip.in new file mode 100644 index 00000000000..e98062776f1 --- /dev/null +++ b/python/core/auto_generated/metadata/qgslayermetadataproviderregistry.sip.in @@ -0,0 +1,73 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/metadata/qgslayermetadataproviderregistry.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + +%ModuleHeaderCode +#include "qgsabstractlayermetadataprovider.h" +%End + +class QgsLayerMetadataProviderRegistry : QObject +{ +%Docstring(signature="appended") +Registry of layer metadata provider backends. + +This is a singleton that should be accessed through :py:func:`QgsApplication.layerMetadataProviderRegistry()`. + +.. seealso:: :py:class:`QgsAbstractLayerMetadataProvider` + +.. versionadded:: 3.28 +%End + +%TypeHeaderCode +#include "qgslayermetadataproviderregistry.h" +%End + public: + + explicit QgsLayerMetadataProviderRegistry( QObject *parent = 0 ); +%Docstring +Creates the layer metadata provider registry, with an optional ``parent`` +%End + + void registerLayerMetadataProvider( QgsAbstractLayerMetadataProvider *metadataProvider /Transfer/ ); +%Docstring +Registers a layer metadata provider ``metadataProvider`` and takes ownership of it +%End + + void unregisterLayerMetadataProvider( QgsAbstractLayerMetadataProvider *metadataProvider ); +%Docstring +Unregisters a layer metadata provider ``metadataProvider`` and destroys its instance +%End + + QList layerMetadataProviders() const; +%Docstring +Returns the list of all registered layer metadata providers. +%End + + QgsAbstractLayerMetadataProvider *layerMetadataProviderFromId( const QString &id ); +%Docstring +Returns metadata provider implementation if the ``id`` matches one. Returns ``None`` otherwise. +%End + + const QgsLayerMetadataSearchResults search( const QgsMetadataSearchContext &searchContext, const QString &searchString = QString(), const QgsRectangle &geographicExtent = QgsRectangle(), QgsFeedback *feedback = 0 ); +%Docstring +Search for layers in all the registered layer metadata providers, optionally filtering by ``searchString`` +and ``geographicExtent``, an optional ``feedback`` can be used to monitor and control the search process. +%End + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/metadata/qgslayermetadataproviderregistry.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/core/auto_generated/providers/qgsabstractdatabaseproviderconnection.sip.in b/python/core/auto_generated/providers/qgsabstractdatabaseproviderconnection.sip.in index 4e3d7bb2528..b9624f79804 100644 --- a/python/core/auto_generated/providers/qgsabstractdatabaseproviderconnection.sip.in +++ b/python/core/auto_generated/providers/qgsabstractdatabaseproviderconnection.sip.in @@ -803,6 +803,31 @@ Returns a SQL query builder for the connection, which provides an interface for The caller takes ownership of the returned object. +.. versionadded:: 3.28 +%End + + virtual QList searchLayerMetadata( const QgsMetadataSearchContext &searchContext, const QString &searchString = QString(), const QgsRectangle &geographicExtent = QgsRectangle(), QgsFeedback *feedback = 0 ) const throw( QgsProviderConnectionException, QgsNotSupportedException ); +%Docstring +Search the stored layer metadata in the connection, +optionally limiting the search to the metadata identifier, title, +abstract, keywords and categories. +``searchContext`` context for the search +``searchString`` limit the search to metadata having an extent intersecting ``geographicExtent``, +an optional ``feedback`` can be used to monitor and control the search process. + +The default implementation raises a :py:class:`QgsNotSupportedException`, data providers may implement +the search functionality. + +A :py:class:`QgsProviderConnectionException` is raised in case of errors happening during the search for +providers that implement the search functionality. + +:return: a (possibly empty) list of :py:class:`QgsLayerMetadataProviderResult`, throws a :py:class:`QgsProviderConnectionException` + if any error occurred during the search. + +:raises QgsProviderConnectionException: + +:raises QgsNotSupportedException: + .. versionadded:: 3.28 %End diff --git a/python/core/auto_generated/qgsapplication.sip.in b/python/core/auto_generated/qgsapplication.sip.in index 57c541e783a..14a8aec408c 100644 --- a/python/core/auto_generated/qgsapplication.sip.in +++ b/python/core/auto_generated/qgsapplication.sip.in @@ -974,6 +974,13 @@ Gets the registry of available scalebar renderers. Returns registry of available project storage implementations. .. versionadded:: 3.2 +%End + + static QgsLayerMetadataProviderRegistry *layerMetadataProviderRegistry() /KeepReference/; +%Docstring +Returns registry of available layer metadata provider implementations. + +.. versionadded:: 3.28 %End static QgsExternalStorageRegistry *externalStorageRegistry() /KeepReference/; diff --git a/python/core/core_auto.sip b/python/core/core_auto.sip index 073736ccb01..59bffacaab4 100644 --- a/python/core/core_auto.sip +++ b/python/core/core_auto.sip @@ -499,6 +499,8 @@ %Include auto_generated/metadata/qgslayermetadatavalidator.sip %Include auto_generated/metadata/qgsmetadatautils.sip %Include auto_generated/metadata/qgsprojectmetadata.sip +%Include auto_generated/metadata/qgsabstractlayermetadataprovider.sip +%Include auto_generated/metadata/qgslayermetadataproviderregistry.sip %Include auto_generated/network/qgsblockingnetworkrequest.sip %Include auto_generated/network/qgsfiledownloader.sip %Include auto_generated/network/qgsnetworkaccessmanager.sip diff --git a/scripts/sipify.pl b/scripts/sipify.pl index cfda42a39d0..18e9f79265e 100755 --- a/scripts/sipify.pl +++ b/scripts/sipify.pl @@ -513,7 +513,7 @@ sub fix_annotations { $line =~ s/SIP_PYNAME\(\s*(\w+)\s*\)/\/PyName=$1\//; $line =~ s/SIP_TYPEHINT\(\s*([\w\.\s,\[\]]+?)\s*\)/\/TypeHint="$1"\//g; $line =~ s/SIP_VIRTUALERRORHANDLER\(\s*(\w+)\s*\)/\/VirtualErrorHandler=$1\//; - $line =~ s/SIP_THROW\(\s*(\w+)\s*\)/throw\( $1 \)/; + $line =~ s/SIP_THROW\(\s*([\w\s,]+?)\s*\)/throw\( $1 \)/; # combine multiple annotations # https://regex101.com/r/uvCt4M/5 diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index f3f0ad0ebb4..37e1e49a93a 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -166,6 +166,8 @@ set(QGIS_CORE_SRCS metadata/qgslayermetadataformatter.cpp metadata/qgsmetadatautils.cpp metadata/qgsprojectmetadata.cpp + metadata/qgsabstractlayermetadataprovider.cpp + metadata/qgslayermetadataproviderregistry.cpp numericformats/qgsbasicnumericformat.cpp numericformats/qgsbearingnumericformat.cpp @@ -279,6 +281,7 @@ set(QGIS_CORE_SRCS providers/meshmemory/qgsmeshmemorydataprovider.cpp + providers/ogr/qgsogrlayermetadataprovider.cpp providers/ogr/qgsogrprovider.cpp providers/ogr/qgsogrprovidermetadata.cpp providers/ogr/qgsogrproviderutils.cpp @@ -1597,6 +1600,8 @@ set(QGIS_CORE_HDRS metadata/qgslayermetadatavalidator.h metadata/qgsmetadatautils.h metadata/qgsprojectmetadata.h + metadata/qgsabstractlayermetadataprovider.h + metadata/qgslayermetadataproviderregistry.h network/qgsblockingnetworkrequest.h network/qgsfiledownloader.h @@ -1693,6 +1698,7 @@ set(QGIS_CORE_HDRS providers/meshmemory/qgsmeshmemorydataprovider.h + providers/ogr/qgsogrlayermetadataprovider.h providers/ogr/qgsgeopackagedataitems.h providers/ogr/qgsgeopackageprojectstorage.h providers/ogr/qgsgeopackageproviderconnection.h diff --git a/src/core/metadata/qgsabstractlayermetadataprovider.cpp b/src/core/metadata/qgsabstractlayermetadataprovider.cpp new file mode 100644 index 00000000000..5182c7ab3f9 --- /dev/null +++ b/src/core/metadata/qgsabstractlayermetadataprovider.cpp @@ -0,0 +1,118 @@ +/*************************************************************************** + qgsabstractlayermetadataprovider.cpp - QgsAbstractLayerMetadataProvider + + --------------------- + begin : 17.8.2022 + copyright : (C) 2022 by Alessandro Pasotti + email : elpaso at itopen dot it + *************************************************************************** + * * + * 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 "qgsabstractlayermetadataprovider.h" +#include "qgsprovidermetadata.h" +#include "qgsproviderregistry.h" +#include "qgsfeedback.h" + + +QList QgsLayerMetadataSearchResults::metadata() const +{ + return mMetadata; +} + +void QgsLayerMetadataSearchResults::addMetadata( const QgsLayerMetadataProviderResult &metadata ) +{ + mMetadata.push_back( metadata ); +} + + +QStringList QgsLayerMetadataSearchResults::errors() const +{ + return mErrors; +} + +void QgsLayerMetadataSearchResults::addError( const QString &error ) +{ + mErrors.push_back( error ); +} + + +QgsLayerMetadataProviderResult::QgsLayerMetadataProviderResult( const QgsLayerMetadata &metadata ) + : QgsLayerMetadata( metadata ) +{ + +} + +const QgsPolygon &QgsLayerMetadataProviderResult::geographicExtent() const +{ + return mGeographicExtent; +} + +void QgsLayerMetadataProviderResult::setGeographicExtent( const QgsPolygon &geographicExtent ) +{ + mGeographicExtent = geographicExtent; +} + +const QgsWkbTypes::GeometryType &QgsLayerMetadataProviderResult::geometryType() const +{ + return mGeometryType; +} + +void QgsLayerMetadataProviderResult::setGeometryType( const QgsWkbTypes::GeometryType &geometryType ) +{ + mGeometryType = geometryType; +} + +const QString &QgsLayerMetadataProviderResult::authid() const +{ + return mAuthid; +} + +void QgsLayerMetadataProviderResult::setAuthid( const QString &authid ) +{ + mAuthid = authid; +} + +const QString &QgsLayerMetadataProviderResult::uri() const +{ + return mUri; +} + +void QgsLayerMetadataProviderResult::setUri( const QString &newUri ) +{ + mUri = newUri; +} + +const QString &QgsLayerMetadataProviderResult::dataProviderName() const +{ + return mDataProviderName; +} + +void QgsLayerMetadataProviderResult::setDataProviderName( const QString &dataProviderName ) +{ + mDataProviderName = dataProviderName; +} + +QgsMapLayerType QgsLayerMetadataProviderResult::layerType() const +{ + return mLayerType; +} + +void QgsLayerMetadataProviderResult::setLayerType( QgsMapLayerType layerType ) +{ + mLayerType = layerType; +} + +const QString &QgsLayerMetadataProviderResult::standardUri() const +{ + return mStandardUri; +} + +void QgsLayerMetadataProviderResult::setStandardUri( const QString &standardUri ) +{ + mStandardUri = standardUri; +} diff --git a/src/core/metadata/qgsabstractlayermetadataprovider.h b/src/core/metadata/qgsabstractlayermetadataprovider.h new file mode 100644 index 00000000000..f8a665e0f8b --- /dev/null +++ b/src/core/metadata/qgsabstractlayermetadataprovider.h @@ -0,0 +1,232 @@ +/*************************************************************************** + qgslayermetadataprovider.h - QgsLayerMetadataProvider + + --------------------- + begin : 17.8.2022 + copyright : (C) 2022 by Alessandro Pasotti + email : elpaso at itopen dot it + *************************************************************************** + * * + * 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 QGSABSTRACTLAYERMETADATAPROVIDER_H +#define QGSABSTRACTLAYERMETADATAPROVIDER_H + +#include + +#include "qgis_core.h" +#include "qgis.h" + +#include "qgslayermetadata.h" +#include "qgsrectangle.h" +#include "qgspolygon.h" +#include "qgscoordinatetransformcontext.h" + + +class QgsFeedback; + +/** + * \ingroup core + * \brief Metadata search context + * \since QGIS 3.28 + */ +struct CORE_EXPORT QgsMetadataSearchContext +{ + //! Coordinate transform context + QgsCoordinateTransformContext transformContext; +}; + +/** + * \ingroup core + * \brief Result record of layer metadata provider search. + * The result contains QGIS metadata information and all information + * that is required by QGIS to load the layer and to filter + * the results. + * + * The class extends QgsLayerMetadata by adding information + * taken directly from the provider which is required for + * filtering (geographic extent) or because the actual + * values may be different by those stored in the metadata + * (CRS authid) or totally missing from the metadata + * (data provider name and layer type). + * + * \since QGIS 3.28 + */ +class CORE_EXPORT QgsLayerMetadataProviderResult: public QgsLayerMetadata +{ + + public: + + /** + * Constructor for QgsLayerMetadataProviderResult. + * \param metadata layer metadata. + */ + QgsLayerMetadataProviderResult( const QgsLayerMetadata &metadata ); + + /** + * Returns the layer extent in EPSG:4326 + */ + const QgsPolygon &geographicExtent() const; + + /** + * Sets the layer extent in EPSG:4326 to \a geographicExtent + */ + void setGeographicExtent( const QgsPolygon &geographicExtent ); + + /** + * Returns the layer geometry type. + */ + const QgsWkbTypes::GeometryType &geometryType() const; + + /** + * Sets the layer geometry type to \a geometryType. + */ + void setGeometryType( const QgsWkbTypes::GeometryType &geometryType ); + + /** + * Returns the layer CRS authid. + */ + const QString &authid() const; + + /** + * Sets the layer \a authid. + */ + void setAuthid( const QString &authid ); + + /** + * Returns the layer data source URI. + */ + const QString &uri() const; + + /** + * Sets the layer data source URI to \a Uri. + */ + void setUri( const QString &Uri ); + + /** + * Returns the data provider name. + */ + const QString &dataProviderName() const; + + /** + * Sets the data provider name to \a dataProviderName. + */ + void setDataProviderName( const QString &dataProviderName ); + + /** + * Returns the layer type. + */ + QgsMapLayerType layerType() const; + + /** + * Sets the layer type to \a layerType. + */ + void setLayerType( QgsMapLayerType layerType ); + + /** + * Returns the metadata standard URI (usually "http://mrcc.com/qgis.dtd") + */ + const QString &standardUri() const; + + /** + * Sets the metadata standard URI to \a standardUri. + */ + void setStandardUri( const QString &standardUri ); + + private: + + //! Layer spatial extent of the layer in EPSG:4326 + QgsPolygon mGeographicExtent; + //! Layer geometry type (Point, Polygon, Linestring) + QgsWkbTypes::GeometryType mGeometryType; + //! Layer CRS authid + QString mAuthid; + //! Layer QgsDataSourceUri string + QString mUri; + //! Layer data provider name + QString mDataProviderName; + //! Layer type (vector, raster etc.) + QgsMapLayerType mLayerType; + //! Metadata standard uri, QGIS QMD metadata format uses "http://mrcc.com/qgis.dtd" + QString mStandardUri; +}; + +/** + * \ingroup core + * \brief Container of result records from a layer metadata search. + * + * Contains the records of the layer metadata provider that matched the + * search criteria and the list of the errors that occurred while + * searching for metadata. + * + * \since QGIS 3.28 + */ +class CORE_EXPORT QgsLayerMetadataSearchResults +{ + + public: + + /** + * Returns the list of metadata results. + */ + QList metadata() const; + + /** + * Adds a \a Metadata record to the list of results. + */ + void addMetadata( const QgsLayerMetadataProviderResult &metadata ); + + /** + * Returns the list of errors occurred during a metadata search. + */ + QStringList errors() const; + + /** + * Adds a \a error to the list of errors. + */ + void addError( const QString &error ); + + private: + + //! List of metadata that matched the search criteria + QList mMetadata; + //! List of errors occurred while searching + QStringList mErrors; +}; + +/** + * \ingroup core + * \brief Layer metadata provider backend interface. + * + * \since QGIS 3.28 + */ +class CORE_EXPORT QgsAbstractLayerMetadataProvider +{ + + public: + + /** + * Returns the id of the layer metadata provider implementation, usually the name of the data provider + * but it may be another unique identifier. + */ + virtual QString id() const = 0; + + /** + * Searches for metadata optionally filtering by search string and geographic extent. + * \param searchContext context for the metadata search. + * \param searchString defines a filter to limit the results to the records where the search string appears in the "identifier", "title" or "abstract" metadata fields, a case-insensitive comparison is used for the match. + * \param geographicExtent defines a filter where the spatial extent matches the given extent in EPSG:4326 + * \param feedback can be used to monitor and control the search process. + * \returns a QgsLayerMetadataSearchResult object with a list of metadata and errors + */ + virtual QgsLayerMetadataSearchResults search( const QgsMetadataSearchContext &searchContext, const QString &searchString = QString(), const QgsRectangle &geographicExtent = QgsRectangle(), QgsFeedback *feedback = nullptr ) const = 0; + + virtual ~QgsAbstractLayerMetadataProvider() = default; + +}; + +#endif // QGSABSTRACTLAYERMETADATAPROVIDER_H diff --git a/src/core/metadata/qgslayermetadataproviderregistry.cpp b/src/core/metadata/qgslayermetadataproviderregistry.cpp new file mode 100644 index 00000000000..9709c09013d --- /dev/null +++ b/src/core/metadata/qgslayermetadataproviderregistry.cpp @@ -0,0 +1,70 @@ +/*************************************************************************** + qgslayermetadataproviderregistry.cpp - QgsLayerMetadataProviderRegistry + + --------------------- + begin : 17.8.2022 + copyright : (C) 2022 by Alessandro Pasotti + email : elpaso at itopen dot it + *************************************************************************** + * * + * 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 "qgslayermetadataproviderregistry.h" +#include "qgsabstractlayermetadataprovider.h" +#include "qgsfeedback.h" + +QgsLayerMetadataProviderRegistry::QgsLayerMetadataProviderRegistry( QObject *parent ) + : QObject( parent ) +{ + +} + +void QgsLayerMetadataProviderRegistry::registerLayerMetadataProvider( QgsAbstractLayerMetadataProvider *metadataProvider ) +{ + mMetadataProviders.insert( metadataProvider->id(), metadataProvider ); +} + +void QgsLayerMetadataProviderRegistry::unregisterLayerMetadataProvider( QgsAbstractLayerMetadataProvider *metadataProvider ) +{ + delete mMetadataProviders.take( metadataProvider->id() ); +} + +QList QgsLayerMetadataProviderRegistry::layerMetadataProviders() const +{ + return mMetadataProviders.values(); +} + +QgsAbstractLayerMetadataProvider *QgsLayerMetadataProviderRegistry::layerMetadataProviderFromId( const QString &type ) +{ + return mMetadataProviders.value( type, nullptr ); +} + +const QgsLayerMetadataSearchResults QgsLayerMetadataProviderRegistry::search( const QgsMetadataSearchContext &searchContext, const QString &searchString, const QgsRectangle &geographicExtent, QgsFeedback *feedback ) +{ + QgsLayerMetadataSearchResults results; + for ( auto it = mMetadataProviders.cbegin(); it != mMetadataProviders.cend(); ++it ) + { + + if ( feedback && feedback->isCanceled() ) + { + break; + } + + const QgsLayerMetadataSearchResults providerResults { it.value()->search( searchContext, searchString, geographicExtent ) }; + const QList constMetadata { providerResults.metadata() }; + for ( const QgsLayerMetadataProviderResult &metadata : std::as_const( constMetadata ) ) + { + results.addMetadata( metadata ); + } + const QList constErrors { providerResults.errors() }; + for ( const QString &error : std::as_const( constErrors ) ) + { + results.addError( error ); + } + } + return results; +} diff --git a/src/core/metadata/qgslayermetadataproviderregistry.h b/src/core/metadata/qgslayermetadataproviderregistry.h new file mode 100644 index 00000000000..0fe30e1b586 --- /dev/null +++ b/src/core/metadata/qgslayermetadataproviderregistry.h @@ -0,0 +1,77 @@ +/*************************************************************************** + qgslayermetadataproviderregistry.h - QgsLayerMetadataProviderRegistry + + --------------------- + begin : 17.8.2022 + copyright : (C) 2022 by Alessandro Pasotti + email : elpaso at itopen dot it + *************************************************************************** + * * + * 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 QGSLAYERMETADATAPROVIDERREGISTRY_H +#define QGSLAYERMETADATAPROVIDERREGISTRY_H + +#include + +#include "qgis_core.h" +#include "qgis.h" + +#include "qgslayermetadata.h" +#include "qgsabstractlayermetadataprovider.h" + +class QgsFeedback; + +#ifdef SIP_RUN +% ModuleHeaderCode +#include "qgsabstractlayermetadataprovider.h" +% End +#endif + +/** + * \ingroup core + * \brief Registry of layer metadata provider backends. + * + * This is a singleton that should be accessed through QgsApplication::layerMetadataProviderRegistry(). + * + * \see QgsAbstractLayerMetadataProvider + * \since QGIS 3.28 + */ +class CORE_EXPORT QgsLayerMetadataProviderRegistry : public QObject +{ + + Q_OBJECT + public: + + //! Creates the layer metadata provider registry, with an optional \a parent + explicit QgsLayerMetadataProviderRegistry( QObject *parent = nullptr ); + + //! Registers a layer metadata provider \a metadataProvider and takes ownership of it + void registerLayerMetadataProvider( QgsAbstractLayerMetadataProvider *metadataProvider SIP_TRANSFER ); + + //! Unregisters a layer metadata provider \a metadataProvider and destroys its instance + void unregisterLayerMetadataProvider( QgsAbstractLayerMetadataProvider *metadataProvider ); + + //! Returns the list of all registered layer metadata providers. + QList layerMetadataProviders() const; + + //! Returns metadata provider implementation if the \a id matches one. Returns NULLPTR otherwise. + QgsAbstractLayerMetadataProvider *layerMetadataProviderFromId( const QString &id ); + + /** + * Search for layers in all the registered layer metadata providers, optionally filtering by \a searchString + * and \a geographicExtent, an optional \a feedback can be used to monitor and control the search process. + */ + const QgsLayerMetadataSearchResults search( const QgsMetadataSearchContext &searchContext, const QString &searchString = QString(), const QgsRectangle &geographicExtent = QgsRectangle(), QgsFeedback *feedback = nullptr ); + + private: + + QHash mMetadataProviders; + +}; + +#endif // QGSLAYERMETADATAPROVIDERREGISTRY_H diff --git a/src/core/providers/ogr/qgsgeopackageproviderconnection.cpp b/src/core/providers/ogr/qgsgeopackageproviderconnection.cpp index 55fabb1d444..bc0f20b4f06 100644 --- a/src/core/providers/ogr/qgsgeopackageproviderconnection.cpp +++ b/src/core/providers/ogr/qgsgeopackageproviderconnection.cpp @@ -28,6 +28,7 @@ #include "qgsfeedback.h" #include "qgsogrutils.h" #include "qgsfielddomain.h" +#include "qgscoordinatetransform.h" #include #include @@ -396,6 +397,138 @@ QString QgsGeoPackageProviderConnection::primaryKeyColumnName( const QString &ta return pkName; } +QList QgsGeoPackageProviderConnection::searchLayerMetadata( const QgsMetadataSearchContext &searchContext, const QString &searchString, const QgsRectangle &geographicExtent, QgsFeedback *feedback ) const +{ + + QList results; + if ( ! feedback || ! feedback->isCanceled() ) + { + try + { + const QString searchQuery { QStringLiteral( R"SQL( + SELECT + ref.table_name, md.metadata, gc.geometry_type_name + FROM + gpkg_metadata_reference AS ref + JOIN + gpkg_metadata AS md ON md.id = ref.md_file_id + JOIN + gpkg_geometry_columns AS gc ON gc.table_name = ref.table_name + WHERE + md.md_standard_uri = 'http://mrcc.com/qgis.dtd' + AND ref.reference_scope = 'table' + AND md.md_scope = 'dataset' + )SQL" ) }; + + const QList constMetadataResults { executeSql( searchQuery, feedback ) }; + for ( const QVariantList &mdRow : std::as_const( constMetadataResults ) ) + { + + if ( feedback && feedback->isCanceled() ) + { + break; + } + + // Read MD from the XML + QDomDocument doc; + doc.setContent( mdRow[1].toString() ); + QgsLayerMetadata layerMetadata; + if ( layerMetadata.readMetadataXml( doc.documentElement() ) ) + { + QgsLayerMetadataProviderResult result{ layerMetadata }; + + QgsRectangle extents; + + const auto cExtents { layerMetadata.extent().spatialExtents() }; + for ( const auto &ext : std::as_const( cExtents ) ) + { + QgsRectangle bbox { ext.bounds.toRectangle() }; + QgsCoordinateTransform ct { ext.extentCrs, QgsCoordinateReferenceSystem::fromEpsgId( 4326 ), searchContext.transformContext }; + ct.transform( bbox ); + extents.combineExtentWith( bbox ); + } + + QgsPolygon poly; + poly.fromWkt( extents.asWktPolygon() ); + + // Filters + if ( ! geographicExtent.isEmpty() && ( poly.isEmpty() || ! geographicExtent.intersects( extents ) ) ) + { + continue; + } + + if ( ! searchString.isEmpty() && ( + ! result.title().contains( searchString, Qt::CaseInsensitive ) && + ! result.identifier().contains( searchString, Qt::CaseInsensitive ) && + ! result.abstract().contains( searchString, Qt::CaseInsensitive ) ) ) + { + bool found { false }; + const QList keyVals { result.keywords().values() }; + for ( const QStringList &kws : std::as_const( keyVals ) ) + { + const QStringList constKws { kws }; + for ( const QString &kw : std::as_const( kws ) ) + { + if ( kw.contains( searchString, Qt::CaseSensitivity::CaseInsensitive ) ) + { + found = true; + break; + } + } + if ( found ) + break; + } + + if ( ! found ) + { + found = result.categories().contains( searchString, Qt::CaseSensitivity::CaseInsensitive ) ; + } + + if ( ! found ) + continue; + } + + result.setGeographicExtent( poly ); + result.setStandardUri( QStringLiteral( "http://mrcc.com/qgis.dtd" ) ); + result.setDataProviderName( QStringLiteral( "ogr" ) ); + result.setAuthid( layerMetadata.crs().authid() ); + result.setUri( tableUri( QString(), mdRow[0].toString() ) ); + const QString geomType { mdRow[2].toString().toUpper() }; + if ( geomType == QStringLiteral( "POINT" ) ) + { + result.setGeometryType( QgsWkbTypes::GeometryType::PointGeometry ); + } + else if ( geomType == QStringLiteral( "POLYGON" ) ) + { + result.setGeometryType( QgsWkbTypes::GeometryType::PolygonGeometry ); + } + else if ( geomType == QStringLiteral( "LINESTRING" ) ) + { + result.setGeometryType( QgsWkbTypes::GeometryType::LineGeometry ); + } + else + { + result.setGeometryType( QgsWkbTypes::GeometryType::UnknownGeometry ); + } + result.setLayerType( QgsMapLayerType::VectorLayer ); + + results.push_back( result ); + } + else + { + throw QgsProviderConnectionException( QStringLiteral( "Error reading XML metdadata from connection %1" ).arg( uri() ) ); + } + } + } + catch ( const QgsProviderConnectionException &ex ) + { + throw QgsProviderConnectionException( QStringLiteral( "Error fetching metdadata from connection %1: %2" ).arg( uri(), ex.what() ) ); + } + } + return results; +} + + QgsFields QgsGeoPackageProviderConnection::fields( const QString &schema, const QString &table ) const { Q_UNUSED( schema ) diff --git a/src/core/providers/ogr/qgsgeopackageproviderconnection.h b/src/core/providers/ogr/qgsgeopackageproviderconnection.h index beba1a58bc1..d21cf9d79de 100644 --- a/src/core/providers/ogr/qgsgeopackageproviderconnection.h +++ b/src/core/providers/ogr/qgsgeopackageproviderconnection.h @@ -50,6 +50,7 @@ class QgsGeoPackageProviderConnection : public QgsOgrProviderConnection QgsFields fields( const QString &schema, const QString &table ) const override; QMultiMap sqlDictionary() override; QList< Qgis::FieldDomainType > supportedFieldDomainTypes() const override; + QList searchLayerMetadata( const QgsMetadataSearchContext &searchContext, const QString &searchString, const QgsRectangle &geographicExtent, QgsFeedback *feedback ) const override; protected: QString databaseQueryLogIdentifier() const override; diff --git a/src/core/providers/ogr/qgsogrlayermetadataprovider.cpp b/src/core/providers/ogr/qgsogrlayermetadataprovider.cpp new file mode 100644 index 00000000000..12284342d9a --- /dev/null +++ b/src/core/providers/ogr/qgsogrlayermetadataprovider.cpp @@ -0,0 +1,63 @@ +/*************************************************************************** + qgsogrlayermetadataprovider.cpp - QgsOgrLayerMetadataProvider + + --------------------- + begin : 24.8.2022 + copyright : (C) 2022 by Alessandro Pasotti + email : elpaso at itopen dot it + *************************************************************************** + * * + * 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 "qgsogrlayermetadataprovider.h" +#include "qgsprovidermetadata.h" +#include "qgsproviderregistry.h" +#include "qgsfeedback.h" +#include "qgsabstractdatabaseproviderconnection.h" + + +QString QgsOgrLayerMetadataProvider::id() const +{ + return QStringLiteral( "ogr" ); +} + +QgsLayerMetadataSearchResults QgsOgrLayerMetadataProvider::search( const QgsMetadataSearchContext &searchContext, const QString &searchString, const QgsRectangle &geographicExtent, QgsFeedback *feedback ) const +{ + QgsLayerMetadataSearchResults results; + QgsProviderMetadata *md { QgsProviderRegistry::instance()->providerMetadata( id( ) ) }; + + if ( md && ( ! feedback || ! feedback->isCanceled( ) ) ) + { + const QMap cConnections { md->connections( ) }; + for ( const QgsAbstractProviderConnection *conn : std::as_const( cConnections ) ) + { + + if ( feedback && feedback->isCanceled() ) + { + break; + } + + if ( const QgsAbstractDatabaseProviderConnection *dbConn = static_cast( conn ) ) + { + try + { + const QList res { dbConn->searchLayerMetadata( searchContext, searchString, geographicExtent, feedback ) }; + for ( const QgsLayerMetadataProviderResult &result : std::as_const( res ) ) + { + results.addMetadata( result ); + } + } + catch ( const QgsProviderConnectionException &ex ) + { + results.addError( QObject::tr( "An error occurred while searching for metadata in connection %1: %2" ).arg( conn->uri(), ex.what() ) ); + } + } + } + } + + return results; +} diff --git a/src/core/providers/ogr/qgsogrlayermetadataprovider.h b/src/core/providers/ogr/qgsogrlayermetadataprovider.h new file mode 100644 index 00000000000..1c12901484b --- /dev/null +++ b/src/core/providers/ogr/qgsogrlayermetadataprovider.h @@ -0,0 +1,29 @@ +/*************************************************************************** + qgsogrlayermetadataprovider.h - QgsOgrLayerMetadataProvider + + --------------------- + begin : 24.8.2022 + copyright : (C) 2022 by Alessandro Pasotti + email : elpaso at itopen dot it + *************************************************************************** + * * + * 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 QGSOGRLAYERMETADATAPROVIDER_H +#define QGSOGRLAYERMETADATAPROVIDER_H + +#define SIP_NO_FILE +#include + +class QgsOgrLayerMetadataProvider : public QgsAbstractLayerMetadataProvider +{ + public: + QString id() const override; + QgsLayerMetadataSearchResults search( const QgsMetadataSearchContext &searchContext, const QString &searchString, const QgsRectangle &geographicExtent, QgsFeedback *feedback = nullptr ) const override; +}; + +#endif // QGSOGRLAYERMETADATAPROVIDER_H diff --git a/src/core/providers/ogr/qgsogrprovider.cpp b/src/core/providers/ogr/qgsogrprovider.cpp index 6a52ce3ad31..4fbf37ada42 100644 --- a/src/core/providers/ogr/qgsogrprovider.cpp +++ b/src/core/providers/ogr/qgsogrprovider.cpp @@ -904,6 +904,9 @@ void QgsOgrProvider::loadFields() void QgsOgrProvider::loadMetadata() { + // Set default, may be overridden by stored metadata + mLayerMetadata.setCrs( crs() ); + if ( mOgrOrigLayer ) { QRecursiveMutex *mutex = nullptr; diff --git a/src/core/providers/ogr/qgsogrprovidermetadata.cpp b/src/core/providers/ogr/qgsogrprovidermetadata.cpp index 0f271e1f8ea..4909618c632 100644 --- a/src/core/providers/ogr/qgsogrprovidermetadata.cpp +++ b/src/core/providers/ogr/qgsogrprovidermetadata.cpp @@ -20,6 +20,8 @@ email : nyall dot dawson at gmail dot com #include "qgssettings.h" #include "qgsmessagelog.h" #include "qgsogrtransaction.h" +#include "qgsogrlayermetadataprovider.h" +#include "qgslayermetadataproviderregistry.h" #include "qgsgeopackageprojectstorage.h" #include "qgsapplication.h" #include "qgsogrconnpool.h" @@ -32,6 +34,8 @@ email : nyall dot dawson at gmail dot com #include "qgsgdalutils.h" #include "qgsproviderregistry.h" #include "qgsvectorfilewriter.h" +#include "qgsvectorlayer.h" +#include "qgsproject.h" #include #include @@ -1044,6 +1048,7 @@ bool QgsOgrProviderMetadata::saveLayerMetadata( const QString &uri, const QgsLay throw QgsNotSupportedException( QObject::tr( "Storing metadata for the specified uri is not supported" ) ); } + QgsTransaction *QgsOgrProviderMetadata::createTransaction( const QString &connString ) { auto ds = QgsOgrProviderUtils::getAlreadyOpenedDataset( connString ); @@ -1058,12 +1063,16 @@ QgsTransaction *QgsOgrProviderMetadata::createTransaction( const QString &connSt } QgsGeoPackageProjectStorage *gGeoPackageProjectStorage = nullptr; // when not null it is owned by QgsApplication::projectStorageRegistry() +QgsOgrLayerMetadataProvider *gOgrLayerMetadataProvider = nullptr; // when not null it is owned by QgsApplication::layerMetadataProviderRegistry() void QgsOgrProviderMetadata::initProvider() { Q_ASSERT( !gGeoPackageProjectStorage ); gGeoPackageProjectStorage = new QgsGeoPackageProjectStorage; QgsApplication::projectStorageRegistry()->registerProjectStorage( gGeoPackageProjectStorage ); // takes ownership + Q_ASSERT( !gOgrLayerMetadataProvider ); + gOgrLayerMetadataProvider = new QgsOgrLayerMetadataProvider(); + QgsApplication::layerMetadataProviderRegistry()->registerLayerMetadataProvider( gOgrLayerMetadataProvider ); // takes ownership } @@ -1071,6 +1080,8 @@ void QgsOgrProviderMetadata::cleanupProvider() { QgsApplication::projectStorageRegistry()->unregisterProjectStorage( gGeoPackageProjectStorage ); // destroys the object gGeoPackageProjectStorage = nullptr; + QgsApplication::layerMetadataProviderRegistry()->unregisterLayerMetadataProvider( gOgrLayerMetadataProvider ); + gOgrLayerMetadataProvider = nullptr; QgsOgrConnPool::cleanupInstance(); // NOTE: QgsApplication takes care of // calling OGRCleanupAll(); diff --git a/src/core/providers/ogr/qgsogrprovidermetadata.h b/src/core/providers/ogr/qgsogrprovidermetadata.h index 2d30407723e..e5a503847c5 100644 --- a/src/core/providers/ogr/qgsogrprovidermetadata.h +++ b/src/core/providers/ogr/qgsogrprovidermetadata.h @@ -22,6 +22,8 @@ email : nyall dot dawson at gmail dot com ///@cond PRIVATE #define SIP_NO_FILE +class QgsLayerMetadataProviderResult; + /** * Entry point for registration of the OGR data provider * \since QGIS 3.10 @@ -83,6 +85,7 @@ class QgsOgrProviderMetadata final: public QgsProviderMetadata QgsAbstractProviderConnection *createConnection( const QString &uri, const QVariantMap &configuration ) override; + }; ///@endcond diff --git a/src/core/providers/qgsabstractdatabaseproviderconnection.cpp b/src/core/providers/qgsabstractdatabaseproviderconnection.cpp index 406c7f70bb0..341f5969288 100644 --- a/src/core/providers/qgsabstractdatabaseproviderconnection.cpp +++ b/src/core/providers/qgsabstractdatabaseproviderconnection.cpp @@ -1080,6 +1080,16 @@ bool QgsAbstractDatabaseProviderConnection::tableExists( const QString &schema, return false; } + +QList QgsAbstractDatabaseProviderConnection::searchLayerMetadata( const QgsMetadataSearchContext &searchContext, const QString &searchString, const QgsRectangle &geographicExtent, QgsFeedback *feedback ) const +{ + Q_UNUSED( feedback ); + Q_UNUSED( searchContext ); + Q_UNUSED( searchString ); + Q_UNUSED( geographicExtent ); + throw QgsNotSupportedException( QObject::tr( "Provider %1 has no %2 method" ).arg( providerKey(), QStringLiteral( "searchLayerMetadata" ) ) ); +} + void QgsAbstractDatabaseProviderConnection::dropRasterTable( const QString &, const QString & ) const { checkCapability( Capability::DropRasterTable ); diff --git a/src/core/providers/qgsabstractdatabaseproviderconnection.h b/src/core/providers/qgsabstractdatabaseproviderconnection.h index 91db90d9ce4..3a9407de933 100644 --- a/src/core/providers/qgsabstractdatabaseproviderconnection.h +++ b/src/core/providers/qgsabstractdatabaseproviderconnection.h @@ -21,6 +21,7 @@ #include "qgis_core.h" #include "qgsfields.h" #include "qgsvectordataprovider.h" +#include "qgsabstractlayermetadataprovider.h" #include @@ -914,6 +915,28 @@ class CORE_EXPORT QgsAbstractDatabaseProviderConnection : public QgsAbstractProv */ virtual QgsProviderSqlQueryBuilder *queryBuilder() const SIP_FACTORY; + /** + * Search the stored layer metadata in the connection, + * optionally limiting the search to the metadata identifier, title, + * abstract, keywords and categories. + * \a searchContext context for the search + * \a searchString limit the search to metadata having an extent intersecting \a geographicExtent, + * an optional \a feedback can be used to monitor and control the search process. + * + * The default implementation raises a QgsNotSupportedException, data providers may implement + * the search functionality. + * + * A QgsProviderConnectionException is raised in case of errors happening during the search for + * providers that implement the search functionality. + * + * \returns a (possibly empty) list of QgsLayerMetadataProviderResult, throws a QgsProviderConnectionException + * if any error occurred during the search. + * \throws QgsProviderConnectionException + * \throws QgsNotSupportedException + * \since QGIS 3.28 + */ + virtual QList searchLayerMetadata( const QgsMetadataSearchContext &searchContext, const QString &searchString = QString(), const QgsRectangle &geographicExtent = QgsRectangle(), QgsFeedback *feedback = nullptr ) const SIP_THROW( QgsProviderConnectionException, QgsNotSupportedException ); + protected: ///@cond PRIVATE diff --git a/src/core/providers/qgsprovidermetadata.cpp b/src/core/providers/qgsprovidermetadata.cpp index e7ad6bda20f..75d24815322 100644 --- a/src/core/providers/qgsprovidermetadata.cpp +++ b/src/core/providers/qgsprovidermetadata.cpp @@ -240,6 +240,7 @@ int QgsProviderMetadata::listStyles( const QString &, QStringList &, QStringList return -1; } + bool QgsProviderMetadata::styleExists( const QString &, const QString &, QString &errorCause ) { errorCause.clear(); @@ -315,7 +316,7 @@ QgsAbstractProviderConnection *QgsProviderMetadata::findConnection( const QStrin QgsAbstractProviderConnection *QgsProviderMetadata::createConnection( const QString &name ) { Q_UNUSED( name ); - throw QgsProviderConnectionException( QObject::tr( "Provider %1 has no %2 method" ).arg( key(), QStringLiteral( "connection" ) ) ); + throw QgsProviderConnectionException( QObject::tr( "Provider %1 has no %2 method" ).arg( key(), QStringLiteral( "createConnection" ) ) ); } @@ -323,7 +324,7 @@ QgsAbstractProviderConnection *QgsProviderMetadata::createConnection( const QStr { Q_UNUSED( configuration ); Q_UNUSED( uri ); - throw QgsProviderConnectionException( QObject::tr( "Provider %1 has no %2 method" ).arg( key(), QStringLiteral( "connection" ) ) ); + throw QgsProviderConnectionException( QObject::tr( "Provider %1 has no %2 method" ).arg( key(), QStringLiteral( "createConnection" ) ) ); } void QgsProviderMetadata::deleteConnection( const QString &name ) diff --git a/src/core/providers/qgsprovidermetadata.h b/src/core/providers/qgsprovidermetadata.h index c8722661efb..54e419a4181 100644 --- a/src/core/providers/qgsprovidermetadata.h +++ b/src/core/providers/qgsprovidermetadata.h @@ -32,6 +32,7 @@ #include "qgis_core.h" #include #include "qgsabstractproviderconnection.h" +#include "qgsabstractlayermetadataprovider.h" #include "qgsfields.h" #include "qgsexception.h" diff --git a/src/core/qgis_sip.h b/src/core/qgis_sip.h index a2b6b9155c5..e4d4e40bd98 100644 --- a/src/core/qgis_sip.h +++ b/src/core/qgis_sip.h @@ -195,7 +195,7 @@ * try/catch blocks around call and catch the correct exception, otherwise only * unknown generic exceptions are available for Python code. */ -#define SIP_THROW(name) +#define SIP_THROW(name, ...) /* * Will insert a `%End` directive in sip files diff --git a/src/core/qgsapplication.cpp b/src/core/qgsapplication.cpp index 7e289531f18..1ed646c04ea 100644 --- a/src/core/qgsapplication.cpp +++ b/src/core/qgsapplication.cpp @@ -20,6 +20,7 @@ #include "qgsexception.h" #include "qgsgeometry.h" #include "qgsannotationitemregistry.h" +#include "qgslayermetadataproviderregistry.h" #include "qgslayout.h" #include "qgslayoutitemregistry.h" #include "qgslogger.h" @@ -2478,6 +2479,11 @@ QgsConnectionRegistry *QgsApplication::connectionRegistry() return members()->mConnectionRegistry; } +QgsLayerMetadataProviderRegistry *QgsApplication::layerMetadataProviderRegistry() +{ + return members()->mLayerMetadataProviderRegistry; +} + QgsPageSizeRegistry *QgsApplication::pageSizeRegistry() { return members()->mPageSizeRegistry; @@ -2515,7 +2521,7 @@ QgsScaleBarRendererRegistry *QgsApplication::scaleBarRendererRegistry() QgsProjectStorageRegistry *QgsApplication::projectStorageRegistry() { - return members()->mProjectStorageRegistry.get(); + return members()->mProjectStorageRegistry; } QgsExternalStorageRegistry *QgsApplication::externalStorageRegistry() @@ -2552,6 +2558,16 @@ QgsApplication::ApplicationMembers::ApplicationMembers() mConnectionRegistry = new QgsConnectionRegistry(); profiler->end(); } + { + profiler->start( tr( "Create project storage registry" ) ); + mProjectStorageRegistry = new QgsProjectStorageRegistry(); + profiler->end(); + } + { + profiler->start( tr( "Create metadata provider registry" ) ); + mLayerMetadataProviderRegistry = new QgsLayerMetadataProviderRegistry(); + profiler->end(); + } { profiler->start( tr( "Create font manager" ) ); mFontManager = new QgsFontManager(); @@ -2682,7 +2698,12 @@ QgsApplication::ApplicationMembers::ApplicationMembers() } { profiler->start( tr( "Setup project storage registry" ) ); - mProjectStorageRegistry.reset( new QgsProjectStorageRegistry() ); + mProjectStorageRegistry = new QgsProjectStorageRegistry(); + profiler->end(); + } + { + profiler->start( tr( "Setup layer metadata provider registry" ) ); + mLayerMetadataProviderRegistry = new QgsLayerMetadataProviderRegistry(); profiler->end(); } { @@ -2759,6 +2780,8 @@ QgsApplication::ApplicationMembers::~ApplicationMembers() delete mNumericFormatRegistry; delete mBookmarkManager; delete mConnectionRegistry; + delete mProjectStorageRegistry; + delete mLayerMetadataProviderRegistry; delete mFontManager; delete mLocalizedDataPathRegistry; delete mCrsRegistry; diff --git a/src/core/qgsapplication.h b/src/core/qgsapplication.h index bbfb8237002..7036f018de3 100644 --- a/src/core/qgsapplication.h +++ b/src/core/qgsapplication.h @@ -39,6 +39,7 @@ class QgsPaintEffectRegistry; class QgsProjectStorageRegistry; class QgsExternalStorageRegistry; class QgsLocalizedDataPathRegistry; +class QgsLayerMetadataProviderRegistry; class QgsRendererRegistry; class QgsSvgCache; class QgsImageCache; @@ -935,6 +936,12 @@ class CORE_EXPORT QgsApplication : public QApplication */ static QgsProjectStorageRegistry *projectStorageRegistry() SIP_KEEPREFERENCE; + /** + * Returns registry of available layer metadata provider implementations. + * \since QGIS 3.28 + */ + static QgsLayerMetadataProviderRegistry *layerMetadataProviderRegistry() SIP_KEEPREFERENCE; + /** * Returns registry of available external storage implementations. * \since QGIS 3.20 @@ -1133,7 +1140,8 @@ class CORE_EXPORT QgsApplication : public QApplication QgsClassificationMethodRegistry *mClassificationMethodRegistry = nullptr; QgsProcessingRegistry *mProcessingRegistry = nullptr; QgsConnectionRegistry *mConnectionRegistry = nullptr; - std::unique_ptr mProjectStorageRegistry; + QgsProjectStorageRegistry *mProjectStorageRegistry = nullptr; + QgsLayerMetadataProviderRegistry *mLayerMetadataProviderRegistry = nullptr; QgsExternalStorageRegistry *mExternalStorageRegistry = nullptr; QgsPageSizeRegistry *mPageSizeRegistry = nullptr; QgsRasterRendererRegistry *mRasterRendererRegistry = nullptr; diff --git a/src/providers/postgres/CMakeLists.txt b/src/providers/postgres/CMakeLists.txt index 4e90e99ebb5..b074d4d845d 100644 --- a/src/providers/postgres/CMakeLists.txt +++ b/src/providers/postgres/CMakeLists.txt @@ -14,6 +14,8 @@ set(PG_SRCS qgspostgresexpressioncompiler.cpp qgspostgreslistener.cpp qgspostgresproviderconnection.cpp + qgspostgreslayermetadataprovider.cpp + qgspostgresprovidermetadatautils.cpp ) if (WITH_GUI) @@ -93,6 +95,7 @@ set(PGRASTER_SRCS raster/qgspostgresrasterutils.cpp qgspostgresconn.cpp qgspostgresconnpool.cpp + qgspostgresprovidermetadatautils.cpp ) # static library diff --git a/src/providers/postgres/qgspostgreslayermetadataprovider.cpp b/src/providers/postgres/qgspostgreslayermetadataprovider.cpp new file mode 100644 index 00000000000..b28caffd5dc --- /dev/null +++ b/src/providers/postgres/qgspostgreslayermetadataprovider.cpp @@ -0,0 +1,68 @@ +/*************************************************************************** + qgspostgreslayermetadataprovider.cpp - QgsPostgresLayerMetadataProvider + + --------------------- + begin : 17.8.2022 + copyright : (C) 2022 by Alessandro Pasotti + email : elpaso at itopen dot it + *************************************************************************** + * * + * 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 "qgspostgreslayermetadataprovider.h" +#include "qgsproviderregistry.h" +#include "qgsprovidermetadata.h" +#include "qgsabstractdatabaseproviderconnection.h" +#include "qgsfeedback.h" + + +QString QgsPostgresLayerMetadataProvider::id() const +{ + return QStringLiteral( "postgres" ); +} + +QgsLayerMetadataSearchResults QgsPostgresLayerMetadataProvider::search( const QgsMetadataSearchContext &searchContext, const QString &searchString, const QgsRectangle &geographicExtent, QgsFeedback *feedback ) const +{ + QgsLayerMetadataSearchResults results; + QgsProviderMetadata *md { QgsProviderRegistry::instance()->providerMetadata( QStringLiteral( "postgres" ) ) }; + + if ( md && ( ! feedback || ! feedback->isCanceled() ) ) + { + const QMap constConnections { md->connections( ) }; + for ( const QgsAbstractProviderConnection *conn : std::as_const( constConnections ) ) + { + + if ( feedback && feedback->isCanceled() ) + { + break; + } + + if ( conn->configuration().value( QStringLiteral( "metadataInDatabase" ), false ).toBool() ) + { + if ( const QgsAbstractDatabaseProviderConnection *dbConn = static_cast( conn ) ) + { + try + { + const QList res { dbConn->searchLayerMetadata( searchContext, searchString, geographicExtent, feedback ) }; + for ( const QgsLayerMetadataProviderResult &result : std::as_const( res ) ) + { + results.addMetadata( result ); + } + } + catch ( const QgsProviderConnectionException &ex ) + { + results.addError( QObject::tr( "An error occurred while searching for metadata in connection %1: %2" ).arg( conn->uri(), ex.what() ) ); + } + } + } + } + } + + return results; +} + + diff --git a/src/providers/postgres/qgspostgreslayermetadataprovider.h b/src/providers/postgres/qgspostgreslayermetadataprovider.h new file mode 100644 index 00000000000..a5657ccb7bc --- /dev/null +++ b/src/providers/postgres/qgspostgreslayermetadataprovider.h @@ -0,0 +1,30 @@ +/*************************************************************************** + qgspostgreslayermetadataprovider.h - QgsPostgresLayerMetadataProvider + + --------------------- + begin : 17.8.2022 + copyright : (C) 2022 by Alessandro Pasotti + email : elpaso at itopen dot it + *************************************************************************** + * * + * 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 QGSPOSTGRESLAYERMETADATAPROVIDER_H +#define QGSPOSTGRESLAYERMETADATAPROVIDER_H + +#include "qgsabstractlayermetadataprovider.h" + +class QgsPostgresLayerMetadataProvider : public QgsAbstractLayerMetadataProvider +{ + public: + + QString id() const override; + + QgsLayerMetadataSearchResults search( const QgsMetadataSearchContext &searchContext, const QString &searchString, const QgsRectangle &geographicExtent, QgsFeedback *feedback = nullptr ) const override; +}; + +#endif // QGSPOSTGRESLAYERMETADATAPROVIDER_H diff --git a/src/providers/postgres/qgspostgresprovider.cpp b/src/providers/postgres/qgspostgresprovider.cpp index df543e2f79c..1cf351a3259 100644 --- a/src/providers/postgres/qgspostgresprovider.cpp +++ b/src/providers/postgres/qgspostgresprovider.cpp @@ -22,6 +22,7 @@ #include "qgsmessageoutput.h" #include "qgsmessagelog.h" #include "qgsprojectstorageregistry.h" +#include "qgslayermetadataproviderregistry.h" #include "qgsrectangle.h" #include "qgscoordinatereferencesystem.h" #include "qgsxmlutils.h" @@ -42,11 +43,13 @@ #include "qgsstringutils.h" #include "qgsjsonutils.h" #include "qgsdbquerylog.h" +#include "qgsproject.h" +#include "qgspostgreslayermetadataprovider.h" #include "qgspostgresprovider.h" #include "qgsprovidermetadata.h" #include "qgspostgresproviderconnection.h" - +#include "qgspostgresprovidermetadatautils.h" #include const QString QgsPostgresProvider::POSTGRES_KEY = QStringLiteral( "postgres" ); @@ -210,6 +213,38 @@ QgsPostgresProvider::QgsPostgresProvider( QString const &uri, const ProviderOpti mLayerExtent.setMinimal(); + // Try to load metadata + const QString schemaQuery = QStringLiteral( "SELECT table_schema FROM information_schema.tables WHERE table_name = 'qgis_layer_metadata'" ); + QgsPostgresResult res( mConnectionRO->LoggedPQexec( "QgsPostgresProvider", schemaQuery ) ); + if ( res.PQntuples( ) > 0 ) + { + const QString schemaName = res.PQgetvalue( 0, 0 ); + // TODO: also filter CRS? + const QString selectQuery = QStringLiteral( R"SQL( + SELECT + qmd + FROM %4.qgis_layer_metadata + WHERE + f_table_schema=%1 + AND f_table_name=%2 + AND f_geometry_column %3 + AND layer_type='vector' + )SQL" ) + .arg( QgsPostgresConn::quotedValue( mUri.schema() ) ) + .arg( QgsPostgresConn::quotedValue( mUri.table() ) ) + .arg( mUri.geometryColumn().isEmpty() ? QStringLiteral( "IS NULL" ) : QStringLiteral( "=%1" ).arg( QgsPostgresConn::quotedValue( mUri.geometryColumn() ) ) ) + .arg( QgsPostgresConn::quotedIdentifier( schemaName ) ); + + QgsPostgresResult res( mConnectionRO->LoggedPQexec( "QgsPostgresProvider", selectQuery ) ); + if ( res.PQntuples() > 0 ) + { + QgsLayerMetadata metadata; + QDomDocument doc; + doc.setContent( res.PQgetvalue( 0, 0 ) ); + mLayerMetadata.readMetadataXml( doc.documentElement() ); + } + } + // set the primary key if ( !determinePrimaryKey() ) { @@ -924,19 +959,22 @@ bool QgsPostgresProvider::loadFields() { QgsDebugMsgLevel( QStringLiteral( "Loading fields for table %1" ).arg( mTableName ), 2 ); - // Get the table description - sql = QStringLiteral( "SELECT description FROM pg_description WHERE objoid=regclass(%1)::oid AND objsubid=0" ).arg( quotedValue( mQuery ) ); - QgsPostgresResult tresult( connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql ) ); - - if ( ! tresult.result() ) + if ( mLayerMetadata.abstract().isEmpty() ) { - throw PGException( tresult ); - } + // Get the table description + sql = QStringLiteral( "SELECT description FROM pg_description WHERE objoid=regclass(%1)::oid AND objsubid=0" ).arg( quotedValue( mQuery ) ); + QgsPostgresResult tresult( connectionRO()->LoggedPQexec( "QgsPostgresProvider", sql ) ); - if ( tresult.PQntuples() > 0 ) - { - mDataComment = tresult.PQgetvalue( 0, 0 ); - mLayerMetadata.setAbstract( mDataComment ); + if ( ! tresult.result() ) + { + throw PGException( tresult ); + } + + if ( tresult.PQntuples() > 0 ) + { + mDataComment = tresult.PQgetvalue( 0, 0 ); + mLayerMetadata.setAbstract( mDataComment ); + } } } @@ -5356,13 +5394,15 @@ bool QgsPostgresProviderMetadata::styleExists( const QString &uri, const QString " WHERE f_table_catalog=%1" " AND f_table_schema=%2" " AND f_table_name=%3" - " AND f_geometry_column=%4" + " AND f_geometry_column %4" " AND (type=%5 OR type IS NULL)" " AND styleName=%6" ) .arg( QgsPostgresConn::quotedValue( dsUri.database() ) ) .arg( QgsPostgresConn::quotedValue( dsUri.schema() ) ) .arg( QgsPostgresConn::quotedValue( dsUri.table() ) ) - .arg( QgsPostgresConn::quotedValue( dsUri.geometryColumn() ) ) + .arg( dsUri.geometryColumn().isEmpty() ? + QStringLiteral( "IS NULL" ) : + QStringLiteral( "= %1" ).arg( QgsPostgresConn::quotedValue( dsUri.geometryColumn() ) ) ) .arg( wkbTypeString ) .arg( QgsPostgresConn::quotedValue( styleId.isEmpty() ? dsUri.table() : styleId ) ); @@ -5488,7 +5528,7 @@ bool QgsPostgresProviderMetadata::saveStyle( const QString &uri, const QString & .arg( QgsPostgresConn::quotedValue( dsUri.database() ) ) .arg( QgsPostgresConn::quotedValue( dsUri.schema() ) ) .arg( QgsPostgresConn::quotedValue( dsUri.table() ) ) - .arg( QgsPostgresConn::quotedValue( dsUri.geometryColumn() ) ) + .arg( dsUri.geometryColumn().isEmpty() ? QStringLiteral( "IS NULL" ) : QStringLiteral( "=%1" ).arg( QgsPostgresConn::quotedValue( dsUri.geometryColumn() ) ) ) .arg( wkbTypeString ) .arg( QgsPostgresConn::quotedValue( styleName.isEmpty() ? dsUri.table() : styleName ) ); @@ -5505,7 +5545,7 @@ bool QgsPostgresProviderMetadata::saveStyle( const QString &uri, const QString & " WHERE f_table_catalog=%6" " AND f_table_schema=%7" " AND f_table_name=%8" - " AND f_geometry_column=%9" + " AND f_geometry_column %9" " AND styleName=%10" " AND (type=%2 OR type IS NULL)" ) .arg( useAsDefault ? "true" : "false" ) @@ -5515,7 +5555,7 @@ bool QgsPostgresProviderMetadata::saveStyle( const QString &uri, const QString & .arg( QgsPostgresConn::quotedValue( dsUri.database() ) ) .arg( QgsPostgresConn::quotedValue( dsUri.schema() ) ) .arg( QgsPostgresConn::quotedValue( dsUri.table() ) ) - .arg( QgsPostgresConn::quotedValue( dsUri.geometryColumn() ) ) + .arg( QgsPostgresConn::quotedValue( dsUri.geometryColumn().isEmpty() ? QStringLiteral( "IS NULL" ) : QStringLiteral( "=%1" ).arg( QgsPostgresConn::quotedValue( dsUri.geometryColumn() ) ) ) ) .arg( QgsPostgresConn::quotedValue( styleName.isEmpty() ? dsUri.table() : styleName ) ) // Must be the final .arg replacement - see above .arg( QgsPostgresConn::quotedValue( qmlStyle ), @@ -5529,12 +5569,12 @@ bool QgsPostgresProviderMetadata::saveStyle( const QString &uri, const QString & " WHERE f_table_catalog=%1" " AND f_table_schema=%2" " AND f_table_name=%3" - " AND f_geometry_column=%4" + " AND f_geometry_column %4" " AND (type=%5 OR type IS NULL)" ) .arg( QgsPostgresConn::quotedValue( dsUri.database() ) ) .arg( QgsPostgresConn::quotedValue( dsUri.schema() ) ) .arg( QgsPostgresConn::quotedValue( dsUri.table() ) ) - .arg( QgsPostgresConn::quotedValue( dsUri.geometryColumn() ) ) + .arg( dsUri.geometryColumn().isEmpty() ? QStringLiteral( "IS NULL" ) : QStringLiteral( "=%1" ).arg( QgsPostgresConn::quotedValue( dsUri.geometryColumn() ) ) ) .arg( wkbTypeString ); sql = QStringLiteral( "BEGIN; %1; %2; COMMIT;" ).arg( removeDefaultSql, sql ); @@ -5690,12 +5730,14 @@ int QgsPostgresProviderMetadata::listStyles( const QString &uri, QStringList &id QString selectOthersQuery = QString( "SELECT id,styleName,description" " FROM layer_styles" - " WHERE NOT (f_table_catalog=%1 AND f_table_schema=%2 AND f_table_name=%3 AND f_geometry_column=%4 AND type=%5)" + " WHERE NOT (f_table_catalog=%1 AND f_table_schema=%2 AND f_table_name=%3 AND f_geometry_column %4 AND type=%5)" " ORDER BY update_time DESC" ) .arg( QgsPostgresConn::quotedValue( dsUri.database() ) ) .arg( QgsPostgresConn::quotedValue( dsUri.schema() ) ) .arg( QgsPostgresConn::quotedValue( dsUri.table() ) ) - .arg( QgsPostgresConn::quotedValue( dsUri.geometryColumn() ) ) + .arg( dsUri.geometryColumn().isEmpty() ? + QStringLiteral( "IS NULL" ) : + QStringLiteral( "=%1" ).arg( QgsPostgresConn::quotedValue( dsUri.geometryColumn() ) ) ) .arg( wkbTypeString ); result = conn->LoggedPQexec( QStringLiteral( "QgsPostgresProviderMetadata" ), selectOthersQuery ); @@ -5819,18 +5861,25 @@ QgsAbstractProviderConnection *QgsPostgresProviderMetadata::createConnection( co QgsPostgresProjectStorage *gPgProjectStorage = nullptr; // when not null it is owned by QgsApplication::projectStorageRegistry() +QgsPostgresLayerMetadataProvider *gPgLayerMetadataProvider = nullptr; // when not null it is owned by QgsApplication::layerMetadataProviderRegistry() void QgsPostgresProviderMetadata::initProvider() { Q_ASSERT( !gPgProjectStorage ); gPgProjectStorage = new QgsPostgresProjectStorage; QgsApplication::projectStorageRegistry()->registerProjectStorage( gPgProjectStorage ); // takes ownership + Q_ASSERT( !gPgLayerMetadataProvider ); + gPgLayerMetadataProvider = new QgsPostgresLayerMetadataProvider(); + QgsApplication::layerMetadataProviderRegistry()->registerLayerMetadataProvider( gPgLayerMetadataProvider ); // takes ownership + } void QgsPostgresProviderMetadata::cleanupProvider() { QgsApplication::projectStorageRegistry()->unregisterProjectStorage( gPgProjectStorage ); // destroys the object gPgProjectStorage = nullptr; + QgsApplication::layerMetadataProviderRegistry()->unregisterLayerMetadataProvider( gPgLayerMetadataProvider ); + gPgLayerMetadataProvider = nullptr; QgsPostgresConnPool::cleanupInstance(); } @@ -6067,3 +6116,14 @@ QList QgsPostgresProviderMetadata::supportedLayerTypes() const { return { QgsMapLayerType::VectorLayer }; } + +bool QgsPostgresProviderMetadata::saveLayerMetadata( const QString &uri, const QgsLayerMetadata &metadata, QString &errorMessage ) +{ + return QgsPostgresProviderMetadataUtils::saveLayerMetadata( QgsMapLayerType::VectorLayer, uri, metadata, errorMessage ); +} + + +QgsProviderMetadata::ProviderCapabilities QgsPostgresProviderMetadata::providerCapabilities() const +{ + return QgsProviderMetadata::ProviderCapability::SaveLayerMetadata; +} diff --git a/src/providers/postgres/qgspostgresprovider.h b/src/providers/postgres/qgspostgresprovider.h index 85d7ec3ba70..7de7daa0904 100644 --- a/src/providers/postgres/qgspostgresprovider.h +++ b/src/providers/postgres/qgspostgresprovider.h @@ -630,6 +630,8 @@ class QgsPostgresProviderMetadata final: public QgsProviderMetadata QVariantMap decodeUri( const QString &uri ) const override; QString encodeUri( const QVariantMap &parts ) const override; QList< QgsMapLayerType > supportedLayerTypes() const override; + bool saveLayerMetadata( const QString &uri, const QgsLayerMetadata &metadata, QString &errorMessage ) override; + QgsProviderMetadata::ProviderCapabilities providerCapabilities() const override; }; // clazy:excludeall=qstring-allocations diff --git a/src/providers/postgres/qgspostgresproviderconnection.cpp b/src/providers/postgres/qgspostgresproviderconnection.cpp index 29ac10d3b03..4a585b6fe8c 100644 --- a/src/providers/postgres/qgspostgresproviderconnection.cpp +++ b/src/providers/postgres/qgspostgresproviderconnection.cpp @@ -16,6 +16,7 @@ #include "qgspostgresproviderconnection.h" #include "qgspostgresconn.h" #include "qgspostgresconnpool.h" +#include "qgspostgresprovidermetadatautils.h" #include "qgssettings.h" #include "qgspostgresprovider.h" #include "qgsexception.h" @@ -32,6 +33,23 @@ extern "C" #include } +// From configuration +const QStringList QgsPostgresProviderConnection::CONFIGURATION_PARAMETERS = +{ + QStringLiteral( "publicOnly" ), + QStringLiteral( "geometryColumnsOnly" ), + QStringLiteral( "dontResolveType" ), + QStringLiteral( "allowGeometrylessTables" ), + QStringLiteral( "saveUsername" ), + QStringLiteral( "savePassword" ), + QStringLiteral( "estimatedMetadata" ), + QStringLiteral( "projectsInDatabase" ), + QStringLiteral( "metadataInDatabase" ), +}; + +const QString QgsPostgresProviderConnection::SETTINGS_BASE_KEY = QStringLiteral( "/PostgreSQL/connections/" ); + + QgsPostgresProviderConnection::QgsPostgresProviderConnection( const QString &name ) : QgsAbstractDatabaseProviderConnection( name ) { @@ -39,6 +57,26 @@ QgsPostgresProviderConnection::QgsPostgresProviderConnection( const QString &nam // Remove the sql and table empty parts const QRegularExpression removePartsRe { R"raw(\s*sql=\s*|\s*table=""\s*)raw" }; setUri( QgsPostgresConn::connUri( name ).uri( false ).replace( removePartsRe, QString() ) ); + + QgsSettings settings; + settings.beginGroup( SETTINGS_BASE_KEY ); + settings.beginGroup( name ); + + QVariantMap config; + + for ( const QString &p : std::as_const( CONFIGURATION_PARAMETERS ) ) + { + const QVariant val = settings.value( p ); + if ( val.isValid() ) + { + config.insert( p, val ); + } + } + + settings.endGroup(); + settings.endGroup(); + + setConfiguration( config ); setDefaultCapabilities(); } @@ -700,12 +738,11 @@ QStringList QgsPostgresProviderConnection::schemas( ) const void QgsPostgresProviderConnection::store( const QString &name ) const { // TODO: move this to class configuration? - QString baseKey = QStringLiteral( "/PostgreSQL/connections/" ); // delete the original entry first remove( name ); QgsSettings settings; - settings.beginGroup( baseKey ); + settings.beginGroup( SETTINGS_BASE_KEY ); settings.beginGroup( name ); // From URI @@ -719,19 +756,7 @@ void QgsPostgresProviderConnection::store( const QString &name ) const settings.setValue( "authcfg", dsUri.authConfigId() ); settings.setEnumValue( "sslmode", dsUri.sslMode() ); - // From configuration - static const QStringList configurationParameters - { - QStringLiteral( "publicOnly" ), - QStringLiteral( "geometryColumnsOnly" ), - QStringLiteral( "dontResolveType" ), - QStringLiteral( "allowGeometrylessTables" ), - QStringLiteral( "saveUsername" ), - QStringLiteral( "savePassword" ), - QStringLiteral( "estimatedMetadata" ), - QStringLiteral( "projectsInDatabase" ) - }; - for ( const auto &p : configurationParameters ) + for ( const auto &p : std::as_const( CONFIGURATION_PARAMETERS ) ) { if ( configuration().contains( p ) ) { @@ -783,6 +808,11 @@ QgsAbstractDatabaseProviderConnection::SqlVectorLayerOptions QgsPostgresProvider return options; } +QList QgsPostgresProviderConnection::searchLayerMetadata( const QgsMetadataSearchContext &searchContext, const QString &searchString, const QgsRectangle &geographicExtent, QgsFeedback *feedback ) const +{ + return QgsPostgresProviderMetadataUtils::searchLayerMetadata( searchContext, uri(), searchString, geographicExtent, feedback ); +} + QgsVectorLayer *QgsPostgresProviderConnection::createSqlVectorLayer( const SqlVectorLayerOptions &options ) const { // Precondition diff --git a/src/providers/postgres/qgspostgresproviderconnection.h b/src/providers/postgres/qgspostgresproviderconnection.h index 2a6a1acb9d2..f1073df6c63 100644 --- a/src/providers/postgres/qgspostgresproviderconnection.h +++ b/src/providers/postgres/qgspostgresproviderconnection.h @@ -79,6 +79,10 @@ class QgsPostgresProviderConnection : public QgsAbstractDatabaseProviderConnecti QgsVectorLayer *createSqlVectorLayer( const SqlVectorLayerOptions &options ) const override; QMultiMap sqlDictionary() override; SqlVectorLayerOptions sqlOptions( const QString &layerSource ) override; + QList searchLayerMetadata( const QgsMetadataSearchContext &searchContext, const QString &searchString, const QgsRectangle &geographicExtent, QgsFeedback *feedback ) const override; + + static const QStringList CONFIGURATION_PARAMETERS; + static const QString SETTINGS_BASE_KEY; private: @@ -88,6 +92,7 @@ class QgsPostgresProviderConnection : public QgsAbstractDatabaseProviderConnecti void dropTablePrivate( const QString &schema, const QString &name ) const; void renameTablePrivate( const QString &schema, const QString &name, const QString &newName ) const; + }; diff --git a/src/providers/postgres/qgspostgresprovidermetadatautils.cpp b/src/providers/postgres/qgspostgresprovidermetadatautils.cpp new file mode 100644 index 00000000000..c91eaedb089 --- /dev/null +++ b/src/providers/postgres/qgspostgresprovidermetadatautils.cpp @@ -0,0 +1,356 @@ +/*************************************************************************** + qgspostgresprovidermetadatautils.cpp - QgsPostgresProviderMetadataUtils + + --------------------- + begin : 29.8.2022 + copyright : (C) 2019 by Alessandro Pasotti + email : elpaso at itopen dot it + *************************************************************************** + * * + * 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 "qgspostgresprovidermetadatautils.h" +#include "qgspostgresproviderconnection.h" +#include "qgscoordinatetransform.h" + + +QList QgsPostgresProviderMetadataUtils::searchLayerMetadata( const QgsMetadataSearchContext &searchContext, const QString &uri, const QString &searchString, const QgsRectangle &geographicExtent, QgsFeedback *feedback ) +{ + Q_UNUSED( searchContext ); + QList results; + QgsDataSourceUri dsUri( uri ); + + QgsPostgresConn *conn = QgsPostgresConn::connectDb( dsUri.connectionInfo( false ), false ); + if ( conn && ( ! feedback || ! feedback->isCanceled() ) ) + { + + QString schemaName { QStringLiteral( "public" ) }; + const QString schemaQuery = QStringLiteral( "SELECT table_schema FROM information_schema.tables WHERE table_name = 'qgis_layer_metadata'" ); + QgsPostgresResult res( conn->LoggedPQexec( "QgsPostgresProviderMetadata", schemaQuery ) ); + if ( res.PQntuples( ) > 0 ) + { + schemaName = res.PQgetvalue( 0, 0 ); + } + + QStringList where; + + if ( ! searchString.isEmpty() ) + { + where.push_back( QStringLiteral( R"SQL(( + abstract ILIKE %1 OR + identifier ILIKE %1 OR + REGEXP_REPLACE(UPPER(array_to_string((xpath('//keyword', qmd))::varchar[], '')),'', '', 'g') ILIKE %1 + ))SQL" ).arg( QgsPostgresConn::quotedValue( QString( searchString ).prepend( QChar( '%' ) ).append( QChar( '%' ) ) ) ) ); + } + + if ( ! geographicExtent.isEmpty() ) + { + where.push_back( QStringLiteral( "ST_Intersects( extent, ST_GeomFromText( %1, 4326 ) )" ).arg( QgsPostgresConn::quotedValue( geographicExtent.asWktPolygon() ) ) ); + } + + const QString listQuery = QStringLiteral( R"SQL( + SELECT + f_table_catalog + ,f_table_schema + ,f_table_name + ,f_geometry_column + ,identifier + ,title + ,abstract + ,geometry_type + ,ST_AsText( extent ) + ,crs + ,layer_type + ,qmd + ,owner + ,update_time + FROM %1.qgis_layer_metadata + %2 + )SQL" ).arg( QgsPostgresConn::quotedIdentifier( schemaName ), QStringLiteral( " WHERE %1 " ).arg( where.join( QStringLiteral( " AND " ) ) ) ); + + res = conn->LoggedPQexec( "QgsPostgresProviderMetadata", listQuery ); + + if ( res.PQresultStatus() != PGRES_TUPLES_OK ) + { + throw QgsProviderConnectionException( QObject::tr( "Error while fetching metadata from %1: %2" ).arg( dsUri.connectionInfo( false ), res.PQresultErrorMessage() ) ); + } + + for ( int row = 0; row < res.PQntuples( ); ++row ) + { + + if ( feedback && feedback->isCanceled() ) + { + break; + } + + QgsLayerMetadata metadata; + QDomDocument doc; + doc.setContent( res.PQgetvalue( 0, 11 ) ); + metadata.readMetadataXml( doc.documentElement() ); + + QgsLayerMetadataProviderResult result { metadata }; + QgsDataSourceUri uri { dsUri }; + uri.setDatabase( res.PQgetvalue( 0, 0 ) ); + uri.setSchema( res.PQgetvalue( 0, 1 ) ); + uri.setTable( res.PQgetvalue( 0, 2 ) ); + uri.setGeometryColumn( res.PQgetvalue( 0, 3 ) ); + result.setStandardUri( QStringLiteral( "http://mrcc.com/qgis.dtd" ) ); + result.setGeometryType( QgsWkbTypes::geometryType( QgsWkbTypes::parseType( res.PQgetvalue( 0, 7 ) ) ) ); + QgsPolygon geographicExtent; + geographicExtent.fromWkt( res.PQgetvalue( 0, 8 ) ); + result.setGeographicExtent( geographicExtent ); + result.setAuthid( res.PQgetvalue( 0, 9 ) ); + const QString layerType { res.PQgetvalue( 0, 10 ) }; + if ( layerType == QStringLiteral( "raster" ) ) + { + result.setDataProviderName( QStringLiteral( "postgresraster" ) ); + result.setLayerType( QgsMapLayerType::RasterLayer ); + } + else if ( layerType == QStringLiteral( "vector" ) ) + { + result.setDataProviderName( QStringLiteral( "postgres" ) ); + result.setLayerType( QgsMapLayerType::VectorLayer ); + } + else + { + QgsDebugMsg( QStringLiteral( "Unsupported layer type '%1': skipping metadata record" ).arg( layerType ) ); + continue; + } + result.setUri( uri.uri() ); + results.append( result ); + } + } + else + { + throw QgsProviderConnectionException( QObject::tr( "Connection to database %1 failed" ).arg( dsUri.connectionInfo( false ) ) ); + } + return results; +} + +bool QgsPostgresProviderMetadataUtils::saveLayerMetadata( const QgsMapLayerType &layerType, const QString &uri, const QgsLayerMetadata &metadata, QString &errorMessage ) +{ + QgsDataSourceUri dsUri( uri ); + + QString layerTypeString; + + if ( layerType == QgsMapLayerType::VectorLayer ) + { + layerTypeString = QStringLiteral( "vector" ); + } + else if ( layerType == QgsMapLayerType::RasterLayer ) + { + layerTypeString = QStringLiteral( "raster" ); + } + else + { + // Unsupported! + return false; + } + + QgsPostgresConn *conn = QgsPostgresConn::connectDb( dsUri.connectionInfo( false ), false ); + if ( !conn ) + { + errorMessage = QObject::tr( "Connection to database failed" ); + return false; + } + + if ( dsUri.database().isEmpty() ) // typically when a service file is used + { + dsUri.setDatabase( conn->currentDatabase() ); + } + + // Try to load metadata + QString schemaName { dsUri.schema().isEmpty() ? QStringLiteral( "public" ) : dsUri.schema() }; + const QString schemaQuery = QStringLiteral( "SELECT table_schema FROM information_schema.tables WHERE table_name = 'qgis_layer_metadata'" ); + QgsPostgresResult res( conn->LoggedPQexec( "QgsPostgresProviderMetadataUtils", schemaQuery ) ); + const bool metadataTableFound { res.PQntuples( ) > 0 }; + if ( metadataTableFound ) + { + schemaName = res.PQgetvalue( 0, 0 ) ; + } + else + { + QgsPostgresResult res( conn->LoggedPQexec( QStringLiteral( "QgsPostgresProviderMetadataUtils" ), + QStringLiteral( R"SQL( + CREATE TABLE %1.qgis_layer_metadata ( + id SERIAL PRIMARY KEY + ,f_table_catalog VARCHAR NOT NULL + ,f_table_schema VARCHAR NOT NULL + ,f_table_name VARCHAR NOT NULL + ,f_geometry_column VARCHAR + ,identifier TEXT NOT NULL + ,title TEXT NOT NULL + ,abstract TEXT + ,geometry_type VARCHAR + ,extent GEOMETRY(POLYGON, 4326) + ,crs VARCHAR + ,layer_type VARCHAR NOT NULL + ,qmd XML NOT NULL + ,owner VARCHAR(63) DEFAULT CURRENT_USER + ,update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE (f_table_catalog, f_table_schema, f_table_name, f_geometry_column, geometry_type, crs, layer_type) + ) + )SQL" ).arg( QgsPostgresConn::quotedIdentifier( schemaName ) ) ) ); + if ( res.PQresultStatus() != PGRES_COMMAND_OK ) + { + errorMessage = QObject::tr( "Unable to save layer metadata. It's not possible to create the destination table on the database. Maybe this is due to table permissions (user=%1). Please contact your database admin" ).arg( dsUri.username() ); + conn->unref(); + return false; + } + } + + const QString wkbTypeString = QgsWkbTypes::geometryDisplayString( QgsWkbTypes::geometryType( dsUri.wkbType() ) ); + + const QgsCoordinateReferenceSystem metadataCrs { metadata.crs() }; + QgsCoordinateReferenceSystem destCrs {QgsCoordinateReferenceSystem::fromEpsgId( 4326 ) }; + QgsRectangle extents; + + const auto cExtents { metadata.extent().spatialExtents() }; + for ( const auto &ext : std::as_const( cExtents ) ) + { + QgsRectangle bbox { ext.bounds.toRectangle() }; + // Note: a default transform context is used here because we don't need high accuracy + + + QgsCoordinateTransform ct { ext.extentCrs, QgsCoordinateReferenceSystem::fromEpsgId( 4326 ), QgsCoordinateTransformContext() }; + ct.transform( bbox ); + extents.combineExtentWith( bbox ); + } + + // export metadata to XML + QDomImplementation domImplementation; + QDomDocumentType documentType = domImplementation.createDocumentType( QStringLiteral( "qgis" ), QStringLiteral( "http://mrcc.com/qgis.dtd" ), QStringLiteral( "SYSTEM" ) ); + QDomDocument document( documentType ); + + QDomElement rootNode = document.createElement( QStringLiteral( "qgis" ) ); + rootNode.setAttribute( QStringLiteral( "version" ), Qgis::version() ); + document.appendChild( rootNode ); + + if ( !metadata.writeMetadataXml( rootNode, document ) ) + { + errorMessage = QObject::tr( "Error exporting metadata to XML" ); + return false; + } + + QString metadataXml; + QTextStream textStream( &metadataXml ); + document.save( textStream, 2 ); + + // Note: in the construction of the INSERT and UPDATE strings the qmd values + // can contain user entered strings, which may themselves include %## values that would be + // replaced by the QString.arg function. To ensure that the final SQL string is not corrupt these + // two values are both replaced in the final .arg call of the string construction. + + QString upsertSql = QStringLiteral( R"SQL( + INSERT INTO %1.qgis_layer_metadata( + f_table_catalog + ,f_table_schema + ,f_table_name + ,f_geometry_column + ,identifier + ,title + ,abstract + ,geometry_type + ,extent + ,crs + ,layer_type + ,qmd) VALUES ( + %2,%3,%4,%5,%6,%7,%8,%9,ST_GeomFromText(%10, 4326),%11,%12,XMLPARSE(DOCUMENT %13)) + )SQL" ) + .arg( QgsPostgresConn::quotedIdentifier( schemaName ) ) + .arg( QgsPostgresConn::quotedValue( dsUri.database() ) ) + .arg( QgsPostgresConn::quotedValue( dsUri.schema() ) ) + .arg( QgsPostgresConn::quotedValue( dsUri.table() ) ) + .arg( QgsPostgresConn::quotedValue( dsUri.geometryColumn() ) ) + .arg( QgsPostgresConn::quotedValue( metadata.identifier() ) ) + .arg( QgsPostgresConn::quotedValue( metadata.title() ) ) + .arg( QgsPostgresConn::quotedValue( metadata.abstract() ) ) + .arg( QgsPostgresConn::quotedValue( wkbTypeString ) ) + .arg( QgsPostgresConn::quotedValue( extents.asWktPolygon() ) ) + .arg( QgsPostgresConn::quotedValue( metadataCrs.authid() ) ) + .arg( QgsPostgresConn::quotedValue( layerTypeString ) ) + // Must be the final .arg replacement - see above + .arg( QgsPostgresConn::quotedValue( metadataXml ) ); + + QString checkQuery = QStringLiteral( R"SQL( + SELECT + f_table_catalog + ,f_table_schema + ,f_table_name + ,f_geometry_column + ,identifier + FROM %1.qgis_layer_metadata + WHERE + f_table_catalog=%2 + AND f_table_schema=%3 + AND f_table_name=%4 + AND f_geometry_column %5 + AND identifier = %6 + AND layer_type = %7 + )SQL" ) + .arg( QgsPostgresConn::quotedIdentifier( schemaName ) ) + .arg( QgsPostgresConn::quotedValue( dsUri.database() ) ) + .arg( QgsPostgresConn::quotedValue( dsUri.schema() ) ) + .arg( QgsPostgresConn::quotedValue( dsUri.table() ) ) + .arg( QgsPostgresConn::quotedValue( dsUri.geometryColumn().isEmpty() ? + QStringLiteral( "IS NULL" ) : + QStringLiteral( "=%1" ).arg( QgsPostgresConn::quotedValue( dsUri.geometryColumn() ) ) ) ) + .arg( QgsPostgresConn::quotedValue( metadata.identifier() ) ) + .arg( QgsPostgresConn::quotedValue( layerTypeString ) ); + + res = conn->LoggedPQexec( "QgsPostgresProviderMetadataUtils", checkQuery ); + if ( res.PQntuples() > 0 ) + { + upsertSql = QStringLiteral( R"SQL( + UPDATE %1.qgis_layer_metadata( + SET + owner=CURRENT_USER + ,title=%8 + ,abstract=%9 + ,geometry_type=%10 + ,extent=ST_GeomFromText(%11, 4326) + ,crs=%12 + ,qmd=XMLPARSE(DOCUMENT %13) + WHERE + f_table_catalog=%2 + AND f_table_schema=%3 + AND f_table_name=%4 + AND f_geometry_column %5 + AND identifier = %6 + AND layer_type = %7 + )SQL" ) + .arg( QgsPostgresConn::quotedIdentifier( schemaName ) ) + .arg( QgsPostgresConn::quotedValue( dsUri.database() ) ) + .arg( QgsPostgresConn::quotedValue( dsUri.schema() ) ) + .arg( QgsPostgresConn::quotedValue( dsUri.table() ) ) + .arg( QgsPostgresConn::quotedValue( dsUri.geometryColumn().isEmpty() ? + QStringLiteral( "IS NULL" ) : + QStringLiteral( "=%1" ).arg( QgsPostgresConn::quotedValue( dsUri.geometryColumn() ) ) ) ) + .arg( QgsPostgresConn::quotedValue( metadata.identifier() ) ) + .arg( QgsPostgresConn::quotedValue( layerTypeString ) ) + .arg( QgsPostgresConn::quotedValue( metadata.title() ) ) + .arg( QgsPostgresConn::quotedValue( metadata.abstract() ) ) + .arg( QgsPostgresConn::quotedValue( wkbTypeString ) ) + .arg( QgsPostgresConn::quotedValue( extents.asWktPolygon() ) ) + .arg( QgsPostgresConn::quotedValue( metadataCrs.authid() ) ) + // Must be the final .arg replacement - see above + .arg( QgsPostgresConn::quotedValue( metadataXml ) ); + + } + + res = conn->LoggedPQexec( "QgsPostgresProviderMetadataUtils", upsertSql ); + + bool saved = res.PQresultStatus() == PGRES_COMMAND_OK; + if ( !saved ) + errorMessage = QObject::tr( "Unable to save layer metadata. It's not possible to insert a new record into the qgis_layer_metadata table. Maybe this is due to table permissions (user=%1). Please contact your database administrator." ).arg( dsUri.username() ); + + conn->unref(); + + return saved; +} + diff --git a/src/providers/postgres/qgspostgresprovidermetadatautils.h b/src/providers/postgres/qgspostgresprovidermetadatautils.h new file mode 100644 index 00000000000..8b56df9dca9 --- /dev/null +++ b/src/providers/postgres/qgspostgresprovidermetadatautils.h @@ -0,0 +1,37 @@ +/*************************************************************************** + qgspostgresprovidermetadatautils.h - QgsPostgresProviderMetadataUtils + + --------------------- + begin : 29.8.2022 + copyright : (C) 2019 by Alessandro Pasotti + email : elpaso at itopen dot it + *************************************************************************** + * * + * 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 QGSPOSTGRESPROVIDERMETADATAUTILS_H +#define QGSPOSTGRESPROVIDERMETADATAUTILS_H + +#include "qgsabstractlayermetadataprovider.h" +#include "qgsrectangle.h" + +class QgsFeedback; + + +/** + * The QgsPostgresProviderMetadataUtils class + * provides utility functions for QgsPostgresProviderMetadata and QgsPostgresRasterProviderMetadata data providers. + */ +class QgsPostgresProviderMetadataUtils +{ + public: + + static QList searchLayerMetadata( const QgsMetadataSearchContext &searchContext, const QString &uri, const QString &searchString, const QgsRectangle &geographicExtent, QgsFeedback *feedback ); + static bool saveLayerMetadata( const QgsMapLayerType &layerType, const QString &uri, const QgsLayerMetadata &metadata, QString &errorMessage ); +}; + +#endif // QGSPOSTGRESPROVIDERMETADATAUTILS_H diff --git a/src/providers/postgres/raster/qgspostgresrasterprovider.cpp b/src/providers/postgres/raster/qgspostgresrasterprovider.cpp index d716a5adf25..2301b505d8d 100644 --- a/src/providers/postgres/raster/qgspostgresrasterprovider.cpp +++ b/src/providers/postgres/raster/qgspostgresrasterprovider.cpp @@ -16,6 +16,8 @@ #include #include "qgspostgresrasterprovider.h" +#include "qgspostgresprovidermetadatautils.h" +#include "qgslayermetadataproviderregistry.h" #include "qgspostgrestransaction.h" #include "qgsmessagelog.h" #include "qgsrectangle.h" @@ -117,9 +119,43 @@ QgsPostgresRasterProvider::QgsPostgresRasterProvider( const QString &uri, const QStringLiteral( "PostGIS" ), Qgis::MessageLevel::Warning ); } + // Try to load metadata + const QString schemaQuery = QStringLiteral( "SELECT table_schema FROM information_schema.tables WHERE table_name = 'qgis_layer_metadata'" ); + QgsPostgresResult res( mConnectionRO->LoggedPQexec( "QgsPostgresRasterProvider", schemaQuery ) ); + if ( res.PQntuples( ) > 0 ) + { + const QString schemaName = res.PQgetvalue( 0, 0 ); + // TODO: also filter CRS? + const QString selectQuery = QStringLiteral( R"SQL( + SELECT + qmd + FROM %4.qgis_layer_metadata + WHERE + f_table_schema=%1 + AND f_table_name=%2 + AND f_geometry_column %3 + AND layer_type='raster' + )SQL" ) + .arg( QgsPostgresConn::quotedValue( mUri.schema() ) ) + .arg( QgsPostgresConn::quotedValue( mUri.table() ) ) + .arg( mUri.geometryColumn().isEmpty() ? QStringLiteral( "IS NULL" ) : QStringLiteral( "=%1" ).arg( QgsPostgresConn::quotedValue( mUri.geometryColumn() ) ) ) + .arg( QgsPostgresConn::quotedIdentifier( schemaName ) ); + + QgsPostgresResult res( mConnectionRO->LoggedPQexec( "QgsPostgresRasterProvider", selectQuery ) ); + if ( res.PQntuples() > 0 ) + { + QgsLayerMetadata metadata; + QDomDocument doc; + doc.setContent( res.PQgetvalue( 0, 0 ) ); + mLayerMetadata.readMetadataXml( doc.documentElement() ); + QgsMessageLog::logMessage( tr( "PostgreSQL raster layer metadata loaded from the database." ), tr( "PostGIS" ) ); + } + } + mLayerMetadata.setType( QStringLiteral( "dataset" ) ); mLayerMetadata.setCrs( crs() ); + mValid = true; } @@ -687,6 +723,16 @@ QList QgsPostgresRasterProviderMetadata::supportedLayerTypes() return { QgsMapLayerType::RasterLayer }; } +bool QgsPostgresRasterProviderMetadata::saveLayerMetadata( const QString &uri, const QgsLayerMetadata &metadata, QString &errorMessage ) +{ + return QgsPostgresProviderMetadataUtils::saveLayerMetadata( QgsMapLayerType::RasterLayer, uri, metadata, errorMessage ); +} + +QgsProviderMetadata::ProviderCapabilities QgsPostgresRasterProviderMetadata::providerCapabilities() const +{ + return QgsProviderMetadata::ProviderCapability::SaveLayerMetadata; +} + QgsPostgresRasterProvider *QgsPostgresRasterProviderMetadata::createProvider( const QString &uri, const QgsDataProvider::ProviderOptions &options, QgsDataProvider::ReadFlags flags ) { return new QgsPostgresRasterProvider( uri, options, flags ); @@ -721,6 +767,11 @@ QgsPostgresRasterProvider *QgsPostgresRasterProvider::clone() const return provider; } +QgsRasterDataProvider::ProviderCapabilities QgsPostgresRasterProvider::providerCapabilities() const +{ + return QgsRasterDataProvider::ProviderCapability::ReadLayerMetadata; +} + static inline QString dumpVariantMap( const QVariantMap &variantMap, const QString &title = QString() ) { @@ -2429,3 +2480,8 @@ QgsFields QgsPostgresRasterProvider::fields() const { return mAttributeFields; } + +QgsLayerMetadata QgsPostgresRasterProvider::layerMetadata() const +{ + return mLayerMetadata; +} diff --git a/src/providers/postgres/raster/qgspostgresrasterprovider.h b/src/providers/postgres/raster/qgspostgresrasterprovider.h index 97860e6ae75..d2e1c09098d 100644 --- a/src/providers/postgres/raster/qgspostgresrasterprovider.h +++ b/src/providers/postgres/raster/qgspostgresrasterprovider.h @@ -67,6 +67,8 @@ class QgsPostgresRasterProvider : public QgsRasterDataProvider virtual QString lastError() override; int capabilities() const override; QgsFields fields() const override; + QgsLayerMetadata layerMetadata() const override; + QgsRasterDataProvider::ProviderCapabilities providerCapabilities() const override; // QgsRasterInterface interface int xSize() const override; @@ -254,6 +256,8 @@ class QgsPostgresRasterProviderMetadata: public QgsProviderMetadata QgsPostgresRasterProvider *createProvider( const QString &uri, const QgsDataProvider::ProviderOptions &options, QgsDataProvider::ReadFlags flags = QgsDataProvider::ReadFlags() ) override; QString encodeUri( const QVariantMap &parts ) const override; QList< QgsMapLayerType > supportedLayerTypes() const override; + bool saveLayerMetadata( const QString &uri, const QgsLayerMetadata &metadata, QString &errorMessage ) override; + QgsProviderMetadata::ProviderCapabilities providerCapabilities() const override; }; diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index cd3b1119066..7ccb84af362 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -143,6 +143,8 @@ ADD_PYTHON_TEST(PyQgsLabelObstacleSettings test_qgslabelobstaclesettings.py) ADD_PYTHON_TEST(PyQgsLabelSettingsWidget test_qgslabelsettingswidget.py) ADD_PYTHON_TEST(PyQgsLabelThinningSettings test_qgslabelthinningsettings.py) ADD_PYTHON_TEST(PyQgsLayerMetadata test_qgslayermetadata.py) +ADD_PYTHON_TEST(PyQgsLayerMetadataProviderPython test_qgslayermetadataprovider_python.py) +ADD_PYTHON_TEST(PyQgsLayerMetadataProviderOgr test_qgslayermetadataprovider_ogr.py) ADD_PYTHON_TEST(PyQgsLayerTreeMapCanvasBridge test_qgslayertreemapcanvasbridge.py) ADD_PYTHON_TEST(PyQgsLayerTree test_qgslayertree.py) ADD_PYTHON_TEST(PyQgsLayerTreeView test_qgslayertreeview.py) @@ -440,6 +442,7 @@ endif() if (ENABLE_PGTEST) ADD_PYTHON_TEST(PyQgsImportIntoPostGIS test_processing_importintopostgis.py) + ADD_PYTHON_TEST(PyQgsLayerMetadataProviderPostgres test_qgslayermetadataprovider_postgres.py) ADD_PYTHON_TEST(PyQgsVectorFileWriterPostgres test_qgsvectorfilewriter_postgres.py) ADD_PYTHON_TEST(PyQgsQueryResultModel test_qgsqueryresultmodel.py) ADD_PYTHON_TEST(PyQgsVectorLayerUtilsPostgres test_qgsvectorlayerutils_postgres.py) @@ -473,7 +476,7 @@ if (ENABLE_PGTEST) PyQgsRelationEditWidget PyQgsRelationPostgres PyQgsVectorLayerTools PyQgsProjectStoragePostgres PyQgsAuthManagerPKIPostgresTest PyQgsAuthManagerPasswordPostgresTest PyQgsAuthManagerOgrPostgresTest PyQgsDbManagerPostgis PyQgsDatabaseSchemaModel PyQgsDatabaseTableModel PyQgsDatabaseSchemaComboBox PyQgsDatabaseTableComboBox - PyQgsProviderConnectionPostgres PyQgsPostgresProviderLatency + PyQgsProviderConnectionPostgres PyQgsPostgresProviderLatency PyQgsLayerMetadataProviderPostgres PROPERTIES LABELS "POSTGRES") endif() diff --git a/tests/src/python/qgslayermetadataprovidertestbase.py b/tests/src/python/qgslayermetadataprovidertestbase.py new file mode 100644 index 00000000000..67027735bc5 --- /dev/null +++ b/tests/src/python/qgslayermetadataprovidertestbase.py @@ -0,0 +1,141 @@ +# coding=utf-8 +""""Base test for layer metadata providers + +.. note:: 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. + +""" + +__author__ = 'elpaso@itopen.it' +__date__ = '2022-08-19' +__copyright__ = 'Copyright 2022, ItOpen' + +import os + +from qgis.core import ( + QgsVectorLayer, + QgsRasterLayer, + QgsMapLayerType, + QgsProviderRegistry, + QgsWkbTypes, + QgsLayerMetadata, + QgsProviderMetadata, + QgsBox3d, + QgsRectangle, + QgsMetadataSearchContext, +) + +from qgis.PyQt.QtCore import QCoreApplication +from utilities import compareWkt, unitTestDataPath +from qgis.testing import start_app + +QGIS_APP = start_app() +TEST_DATA_DIR = unitTestDataPath() + + +class LayerMetadataProviderTestBase(): + """Base test for layer metadata providers + + Provider tests must implement: + - getLayer() -> return a QgsVectorLayer or a QgsRasterLayer + - getMetadataProviderId() -> str returns the id of the metadata provider to be tested ('ogr', 'postgres' ...) + """ + + @classmethod + def setUpClass(cls): + """Run before all tests""" + + QCoreApplication.setOrganizationName("QGIS_Test") + QCoreApplication.setOrganizationDomain(cls.__name__) + QCoreApplication.setApplicationName(cls.__name__) + + def testMetadataWriteRead(self): + + self.test_layer = self.getLayer() + self.assertTrue(self.test_layer.isValid()) + extent_as_wkt = self.test_layer.extent().asWktPolygon() + layer_type = self.test_layer.type() + layer_authid = self.test_layer.crs().authid() + data_provider_name = self.test_layer.dataProvider().name() + + m = self.test_layer.metadata() + m.setAbstract('QGIS Some Data') + m.setIdentifier('MD012345') + m.setTitle('QGIS Test Title') + m.setKeywords({'dtd1': ['Kw1', 'Kw2']}) + m.setCategories(['Cat1', 'Cat2']) + ext = QgsLayerMetadata.Extent() + spatial_ext = QgsLayerMetadata.SpatialExtent() + spatial_ext.bounds = QgsBox3d(self.test_layer.extent()) + spatial_ext.crs = self.test_layer.crs() + ext.setSpatialExtents([spatial_ext]) + m.setExtent(ext) + self.test_layer.setMetadata(m) + + md = QgsProviderRegistry.instance().providerMetadata(data_provider_name) + self.assertIsNotNone(md) + self.assertTrue(bool(md.providerCapabilities() & QgsProviderMetadata.ProviderCapability.SaveLayerMetadata)) + + layer_uri = self.test_layer.publicSource() + self.assertTrue(md.saveLayerMetadata(layer_uri, m)[0]) + + self.test_layer = self.getLayer() + m = self.test_layer.metadata() + self.assertEqual(m.title(), 'QGIS Test Title') + self.assertEqual(m.identifier(), 'MD012345') + self.assertEqual(m.abstract(), 'QGIS Some Data') + self.assertEqual(m.crs().authid(), layer_authid) + + del self.test_layer + + reg = QGIS_APP.layerMetadataProviderRegistry() + md_provider = reg.layerMetadataProviderFromId(self.getMetadataProviderId()) + results = md_provider.search(QgsMetadataSearchContext(), 'QgIs SoMe DaTa') + self.assertEqual(len(results.metadata()), 1) + + result = results.metadata()[0] + + self.assertEqual(result.abstract(), 'QGIS Some Data') + self.assertEqual(result.identifier(), 'MD012345') + self.assertEqual(result.title(), 'QGIS Test Title') + self.assertEqual(result.layerType(), layer_type) + self.assertEqual(result.authid(), layer_authid) + # For raster is unknown + if layer_type != QgsMapLayerType.VectorLayer: + self.assertEqual(result.geometryType(), QgsWkbTypes.UnknownGeometry) + else: + self.assertEqual(result.geometryType(), QgsWkbTypes.PointGeometry) + self.assertEqual(result.dataProviderName(), data_provider_name) + self.assertEqual(result.standardUri(), 'http://mrcc.com/qgis.dtd') + self.assertTrue(compareWkt(result.geographicExtent().asWkt(), extent_as_wkt)) + + # Check layer load + if layer_type == QgsMapLayerType.VectorLayer: + test_layer = QgsVectorLayer(result.uri(), 'PG MD Layer', result.dataProviderName()) + else: + test_layer = QgsRasterLayer(result.uri(), 'PG MD Layer', result.dataProviderName()) + + self.assertTrue(test_layer.isValid()) + + # Test search filters + results = md_provider.search(QgsMetadataSearchContext(), '', QgsRectangle(0, 0, 1, 1)) + self.assertEqual(len(results.metadata()), 0) + results = md_provider.search(QgsMetadataSearchContext(), '', test_layer.extent()) + self.assertEqual(len(results.metadata()), 1) + results = md_provider.search(QgsMetadataSearchContext(), 'NOT HERE!', test_layer.extent()) + self.assertEqual(len(results.metadata()), 0) + results = md_provider.search(QgsMetadataSearchContext(), 'QGIS', test_layer.extent()) + self.assertEqual(len(results.metadata()), 1) + + # Test keywords + results = md_provider.search(QgsMetadataSearchContext(), 'kw') + self.assertEqual(len(results.metadata()), 1) + results = md_provider.search(QgsMetadataSearchContext(), 'kw2') + self.assertEqual(len(results.metadata()), 1) + # Test categories + results = md_provider.search(QgsMetadataSearchContext(), 'cat') + self.assertEqual(len(results.metadata()), 1) + results = md_provider.search(QgsMetadataSearchContext(), 'cat2') + self.assertEqual(len(results.metadata()), 1) diff --git a/tests/src/python/test_qgslayermetadataprovider_ogr.py b/tests/src/python/test_qgslayermetadataprovider_ogr.py new file mode 100644 index 00000000000..266429c318d --- /dev/null +++ b/tests/src/python/test_qgslayermetadataprovider_ogr.py @@ -0,0 +1,56 @@ +# coding=utf-8 +""""Test for ogr layer metadata provider + +.. note:: 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. + +""" + +__author__ = 'elpaso@itopen.it' +__date__ = '2022-08-19' +__copyright__ = 'Copyright 2022, ItOpen' + +import os +import shutil + +from qgis.core import ( + QgsVectorLayer, + QgsProviderRegistry, +) + +from qgis.PyQt.QtCore import QTemporaryDir +from qgis.testing import unittest +from qgslayermetadataprovidertestbase import LayerMetadataProviderTestBase, TEST_DATA_DIR + + +class TestPostgresLayerMetadataProvider(unittest.TestCase, LayerMetadataProviderTestBase): + + def getMetadataProviderId(self) -> str: + + return 'ogr' + + def getLayer(self) -> QgsVectorLayer: + + return QgsVectorLayer('{}|layername=geopackage'.format(self.getConnectionUri()), "someData", 'ogr') + + def getConnectionUri(self) -> str: + + return self.conn + + def setUp(self): + + super().setUp() + self.temp_dir = QTemporaryDir() + self.temp_path = self.temp_dir.path() + srcpath = os.path.join(TEST_DATA_DIR, 'provider') + shutil.copy(os.path.join(srcpath, 'geopackage.gpkg'), self.temp_path) + self.conn = os.path.join(self.temp_path, 'geopackage.gpkg') + md = QgsProviderRegistry.instance().providerMetadata('ogr') + conn = md.createConnection(self.getConnectionUri(), {}) + conn.store('OGR Metadata Enabled Connection') + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/src/python/test_qgslayermetadataprovider_postgres.py b/tests/src/python/test_qgslayermetadataprovider_postgres.py new file mode 100644 index 00000000000..a5588f1dd0c --- /dev/null +++ b/tests/src/python/test_qgslayermetadataprovider_postgres.py @@ -0,0 +1,72 @@ +# coding=utf-8 +""""Test for postgres layer metadata provider + +.. note:: 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. + +""" + +__author__ = 'elpaso@itopen.it' +__date__ = '2022-08-19' +__copyright__ = 'Copyright 2022, ItOpen' + +import os + +from qgis.core import ( + QgsVectorLayer, + QgsProviderRegistry, +) + +from qgis.testing import unittest +from qgslayermetadataprovidertestbase import LayerMetadataProviderTestBase + + +class TestPostgresLayerMetadataProvider(unittest.TestCase, LayerMetadataProviderTestBase): + + def getMetadataProviderId(self) -> str: + + return 'postgres' + + def getLayer(self): + + return QgsVectorLayer('{} type=Point table="qgis_test"."someData" (geom) sql='.format(self.getConnectionUri()), "someData", 'postgres') + + def getConnectionUri(self) -> str: + + dbconn = 'service=qgis_test' + + if 'QGIS_PGTEST_DB' in os.environ: + dbconn = os.environ['QGIS_PGTEST_DB'] + + return dbconn + + def clearMetadataTable(self): + + self.conn.execSql('DROP TABLE IF EXISTS qgis_test.qgis_layer_metadata') + + def setUp(self): + + super().setUp() + + dbconn = 'service=qgis_test' + + if 'QGIS_PGTEST_DB' in os.environ: + dbconn = os.environ['QGIS_PGTEST_DB'] + + md = QgsProviderRegistry.instance().providerMetadata('postgres') + conn = md.createConnection(self.getConnectionUri(), {}) + conn.setConfiguration({'metadataInDatabase': True}) + conn.store('PG Metadata Enabled Connection') + self.conn = conn + self.clearMetadataTable() + + def tearDown(self): + + super().tearDown() + self.clearMetadataTable() + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/src/python/test_qgslayermetadataprovider_postgresraster.py b/tests/src/python/test_qgslayermetadataprovider_postgresraster.py new file mode 100644 index 00000000000..62fb4c540c0 --- /dev/null +++ b/tests/src/python/test_qgslayermetadataprovider_postgresraster.py @@ -0,0 +1,63 @@ +# coding=utf-8 +""""Test for postgres layer metadata provider + +.. note:: 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. + +""" + +__author__ = 'elpaso@itopen.it' +__date__ = '2022-08-19' +__copyright__ = 'Copyright 2022, ItOpen' + +import os + +from qgis.core import ( + QgsRasterLayer, + QgsProviderRegistry, +) + +from qgis.PyQt.QtCore import QCoreApplication +from qgis.testing import unittest +from qgslayermetadataprovidertestbase import LayerMetadataProviderTestBase + + +class TestPostgresLayerMetadataProvider(unittest.TestCase, LayerMetadataProviderTestBase): + + def getMetadataProviderId(self): + + return 'postgres' + + def getLayer(self): + + return QgsRasterLayer('{} table="qgis_test"."Raster1" (Rast)'.format(self.getConnectionUri()), "someData", 'postgresraster') + + def getConnectionUri(self) -> str: + + dbconn = 'service=qgis_test' + + if 'QGIS_PGTEST_DB' in os.environ: + dbconn = os.environ['QGIS_PGTEST_DB'] + + return dbconn + + def setUp(self): + + super().setUp() + + dbconn = 'service=qgis_test' + + if 'QGIS_PGTEST_DB' in os.environ: + dbconn = os.environ['QGIS_PGTEST_DB'] + + md = QgsProviderRegistry.instance().providerMetadata('postgres') + conn = md.createConnection(self.getConnectionUri(), {}) + conn.execSql('DROP TABLE IF EXISTS qgis_test.qgis_layer_metadata') + conn.setConfiguration({'metadataInDatabase': True}) + conn.store('PG Metadata Enabled Connection') + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/src/python/test_qgslayermetadataprovider_python.py b/tests/src/python/test_qgslayermetadataprovider_python.py new file mode 100644 index 00000000000..2ba4d38ccdd --- /dev/null +++ b/tests/src/python/test_qgslayermetadataprovider_python.py @@ -0,0 +1,181 @@ +""""Test for a python implementation of layer metadata provider + +.. note:: 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. + +""" + +__author__ = 'elpaso@itopen.it' +__date__ = '2022-08-19' +__copyright__ = 'Copyright 2022, ItOpen' + +import os +import shutil +from functools import partial +from stat import S_IREAD, S_IRGRP, S_IROTH, S_IWUSR + +from qgis.core import ( + QgsPolygon, + QgsWkbTypes, + QgsRectangle, + QgsMapLayerType, + QgsProviderRegistry, + QgsAbstractLayerMetadataProvider, + QgsLayerMetadataSearchResults, + QgsLayerMetadataProviderResult, + QgsMetadataSearchContext, + QgsLayerMetadata, + QgsNotSupportedException, + QgsProviderConnectionException, +) + +from qgis.PyQt.QtCore import QTemporaryDir +from qgis.PyQt.QtXml import QDomDocument +from qgis.testing import unittest, start_app +from utilities import unitTestDataPath + +TEST_DATA_DIR = unitTestDataPath() + +temp_dir = QTemporaryDir() +temp_path = temp_dir.path() + + +class PythonLayerMetadataProvider(QgsAbstractLayerMetadataProvider): + """Python implementation of a layer metadata provider + This is mainly to test the Python bindings and API + """ + + def __init__(self): + super().__init__() + + def id(self): + return 'python' + + def search(self, searchString='', geographicExtent=QgsRectangle(), feedback=None): + + xml_md = """ + + + MD012345 + + + dataset + QGIS Test Title + QGIS Some Data + + + + + + GEOGCRS["WGS 84",DATUM["World Geodetic System 1984",ELLIPSOID["WGS 84",6378137,298.257223563,LENGTHUNIT["metre",1]]],PRIMEM["Greenwich",0,ANGLEUNIT["degree",0.0174532925199433]],CS[ellipsoidal,2],AXIS["geodetic latitude (Lat)",north,ORDER[1],ANGLEUNIT["degree",0.0174532925199433]],AXIS["geodetic longitude (Lon)",east,ORDER[2],ANGLEUNIT["degree",0.0174532925199433]],USAGE[SCOPE["Horizontal component of 3D system."],AREA["World."],BBOX[-90,-180,90,180]],ID["EPSG",4326]] + +proj=longlat +datum=WGS84 +no_defs + 3452 + 4326 + EPSG:4326 + WGS 84 + longlat + EPSG:7030 + true + + + + + + + """ + + doc = QDomDocument() + assert doc.setContent(xml_md)[0] + + metadata = QgsLayerMetadata() + assert metadata.readMetadataXml(doc.documentElement()) + + result = QgsLayerMetadataProviderResult(metadata) + result.setStandardUri('http://mrcc.com/qgis.dtd') + result.setLayerType(QgsMapLayerType.VectorLayer) + result.setUri(os.path.join(temp_path, 'geopackage.gpkg')) + result.setAuthid('EPSG:4326') + result.setDataProviderName('ogr') + result.setGeometryType(QgsWkbTypes.GeometryType.PointGeometry) + + poly = QgsPolygon() + poly.fromWkt(QgsRectangle(0, 0, 1, 1).asWktPolygon()) + result.setGeographicExtent(poly) + + assert result.identifier() == 'MD012345' + + results = QgsLayerMetadataSearchResults() + results.addMetadata(result) + results.addError('Bad news from PythonLayerMetadataProvider :(') + + return results + + +QGIS_APP = start_app() + + +class TestPythonLayerMetadataProvider(unittest.TestCase): + + def setUp(self): + + super().setUp() + srcpath = os.path.join(TEST_DATA_DIR, 'provider') + shutil.copy(os.path.join(srcpath, 'geopackage.gpkg'), temp_path) + self.conn = os.path.join(temp_path, 'geopackage.gpkg') + + shutil.copy(os.path.join(srcpath, 'spatialite.db'), temp_path) + self.conn_sl = os.path.join(temp_path, 'spatialite.db') + + def test_metadataRegistryApi(self): + + reg = QGIS_APP.layerMetadataProviderRegistry() + self.assertIsNone(reg.layerMetadataProviderFromId('python')) + reg.registerLayerMetadataProvider(PythonLayerMetadataProvider()) + self.assertIsNotNone(reg.layerMetadataProviderFromId('python')) + + md_provider = reg.layerMetadataProviderFromId('python') + results = md_provider.search(QgsMetadataSearchContext()) + + self.assertEqual(len(results.metadata()), 1) + self.assertEqual(len(results.errors()), 1) + + result = results.metadata()[0] + + self.assertEqual(result.abstract(), 'QGIS Some Data') + self.assertEqual(result.identifier(), 'MD012345') + self.assertEqual(result.title(), 'QGIS Test Title') + self.assertEqual(result.layerType(), QgsMapLayerType.VectorLayer) + self.assertEqual(result.authid(), 'EPSG:4326') + self.assertEqual(result.geometryType(), QgsWkbTypes.PointGeometry) + self.assertEqual(result.dataProviderName(), 'ogr') + self.assertEqual(result.standardUri(), 'http://mrcc.com/qgis.dtd') + + reg.unregisterLayerMetadataProvider(md_provider) + self.assertIsNone(reg.layerMetadataProviderFromId('python')) + + def testExceptions(self): + + def _spatialite(path): + + md = QgsProviderRegistry.instance().providerMetadata('spatialite') + conn = md.createConnection(path, {}) + conn.searchLayerMetadata(QgsMetadataSearchContext()) + + def _ogr(path): + + md = QgsProviderRegistry.instance().providerMetadata('ogr') + conn = md.createConnection(path, {}) + os.chmod(path, S_IREAD | S_IRGRP | S_IROTH) + conn.searchLayerMetadata(QgsMetadataSearchContext()) + + self.assertRaises(QgsNotSupportedException, partial(_spatialite, self.conn_sl)) + self.assertRaises(QgsProviderConnectionException, partial(_ogr, self.conn)) + self.assertRaises(QgsNotSupportedException, partial(_ogr, self.conn_sl)) + os.chmod(self.conn, S_IWUSR | S_IREAD) + os.chmod(self.conn_sl, S_IWUSR | S_IREAD) + + +if __name__ == '__main__': + unittest.main()