Skip to content

Commit

Permalink
Implement project storage for PostgreSQL + tests of the storage
Browse files Browse the repository at this point in the history
  • Loading branch information
wonder-sk committed Apr 7, 2018
1 parent 5963028 commit 0f5ea53
Show file tree
Hide file tree
Showing 10 changed files with 382 additions and 0 deletions.
5 changes: 5 additions & 0 deletions python/core/qgsprojectstorageregistry.sip.in
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ This is a singleton that should be accessed through :py:func:`QgsApplication.pro
QgsProjectStorageRegistry();
~QgsProjectStorageRegistry();

QgsProjectStorage *projectStorageFromType( const QString &type );
%Docstring
Returns storage implementation if the storage type matches one. Returns null pointer otherwise (it is a normal file)
%End

QgsProjectStorage *projectStorageFromUri( const QString &uri );
%Docstring
Returns storage implementation if the URI matches one. Returns null pointer otherwise (it is a normal file)
Expand Down
5 changes: 5 additions & 0 deletions src/core/qgsprojectstorageregistry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ QgsProjectStorageRegistry::~QgsProjectStorageRegistry()
qDeleteAll( mBackends.values() );
}

QgsProjectStorage *QgsProjectStorageRegistry::projectStorageFromType( const QString &type )
{
return mBackends.value( type, nullptr );
}

QgsProjectStorage *QgsProjectStorageRegistry::projectStorageFromUri( const QString &uri )
{
for ( auto it = mBackends.constBegin(); it != mBackends.constEnd(); ++it )
Expand Down
3 changes: 3 additions & 0 deletions src/core/qgsprojectstorageregistry.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ class CORE_EXPORT QgsProjectStorageRegistry
QgsProjectStorageRegistry();
~QgsProjectStorageRegistry();

//! Returns storage implementation if the storage type matches one. Returns null pointer otherwise (it is a normal file)
QgsProjectStorage *projectStorageFromType( const QString &type );

//! Returns storage implementation if the URI matches one. Returns null pointer otherwise (it is a normal file)
QgsProjectStorage *projectStorageFromUri( const QString &uri );

Expand Down
6 changes: 6 additions & 0 deletions src/core/qgsproviderregistry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ typedef void buildsupportedrasterfilefilter_t( QString &fileFiltersString );
typedef QString databaseDrivers_t();
typedef QString directoryDrivers_t();
typedef QString protocolDrivers_t();
typedef void initProviderFunction_t();
//typedef int dataCapabilities_t();
//typedef QgsDataItem * dataItem_t(QString);

Expand Down Expand Up @@ -228,6 +229,11 @@ void QgsProviderRegistry::init()

QgsDebugMsg( QString( "Checking %1: ...loaded OK (%2 file filters)" ).arg( myLib.fileName() ).arg( fileRasterFilters.split( ";;" ).count() ) );
}

// call initProvider() if such function is available - allows provider to register its services to QGIS
initProviderFunction_t *initFunc = reinterpret_cast< initProviderFunction_t * >( cast_to_fptr( myLib.resolve( "initProvider" ) ) );
if ( initFunc )
initFunc();
}
} // QgsProviderRegistry ctor

Expand Down
1 change: 1 addition & 0 deletions src/providers/postgres/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ SET(PG_SRCS
qgspostgresconnpool.cpp
qgspostgresdataitems.cpp
qgspostgresfeatureiterator.cpp
qgspostgresprojectstorage.cpp
qgspostgrestransaction.cpp
qgspgtablemodel.cpp
qgscolumntypethread.cpp
Expand Down
188 changes: 188 additions & 0 deletions src/providers/postgres/qgspostgresprojectstorage.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
#include "qgspostgresprojectstorage.h"

#include "qgspostgresconn.h"
#include "qgspostgresconnpool.h"

#include "qgsreadwritecontext.h"

#include <QIODevice>
#include <QUrl>

static bool _projectsTableExists( QgsPostgresConn &conn, const QString &schemaName )
{
QString tableName( "qgis_projects" );
QString sql( QStringLiteral( "SELECT COUNT(*) FROM information_schema.tables WHERE table_name=%1 and table_schema=%2" )
.arg( QgsPostgresConn::quotedValue( tableName ), QgsPostgresConn::quotedValue( schemaName ) )
);
QgsPostgresResult res( conn.PQexec( sql ) );
return res.PQgetvalue( 0, 0 ).toInt() > 0;
}


QgsPostgresProjectStorage::QgsPostgresProjectStorage()
{
}


QStringList QgsPostgresProjectStorage::listProjects( const QString &uri )
{
QStringList lst;

QgsPostgresProjectUri projectUri = parseUri( uri );
if ( !projectUri.valid )
return lst;

QgsPostgresConn *conn = QgsPostgresConnPool::instance()->acquireConnection( projectUri.connInfo.connectionInfo( false ) );

if ( _projectsTableExists( *conn, projectUri.schemaName ) )
{
QString sql( QStringLiteral( "SELECT name FROM %1.qgis_projects" ).arg( QgsPostgresConn::quotedIdentifier( projectUri.schemaName ) ) );
QgsPostgresResult result( conn->PQexec( sql ) );
if ( result.PQresultStatus() == PGRES_TUPLES_OK )
{
int count = result.PQntuples();
for ( int i = 0; i < count; ++i )
{
QString name = result.PQgetvalue( i, 0 );
lst << name;
}
}
}

QgsPostgresConnPool::instance()->releaseConnection( conn );

return lst;
}


bool QgsPostgresProjectStorage::readProject( const QString &uri, QIODevice *device, QgsReadWriteContext &context )
{
QgsPostgresProjectUri projectUri = parseUri( uri );
if ( !projectUri.valid )
{
context.pushMessage( "Invalid URI for PostgreSQL provider: " + uri, Qgis::Critical );
return false;
}

QgsPostgresConn *conn = QgsPostgresConnPool::instance()->acquireConnection( projectUri.connInfo.connectionInfo( false ) );

if ( !_projectsTableExists( *conn, projectUri.schemaName ) )
{
// TODO: write to context
QgsPostgresConnPool::instance()->releaseConnection( conn );
return false;
}

bool ok = false;
QString sql( QStringLiteral( "SELECT content FROM %1.qgis_projects WHERE name = %2" ).arg( QgsPostgresConn::quotedIdentifier( projectUri.schemaName ), QgsPostgresConn::quotedValue( projectUri.projectName ) ) );
QgsPostgresResult result( conn->PQexec( sql ) );
if ( result.PQresultStatus() == PGRES_TUPLES_OK )
{
if ( result.PQntuples() == 1 )
{
// TODO: would be useful to have QByteArray version of PQgetvalue to avoid bytearray -> string -> bytearray conversion
QString hexEncodedContent( result.PQgetvalue( 0, 0 ) );
QByteArray binaryContent( QByteArray::fromHex( hexEncodedContent.toUtf8() ) );
device->write( binaryContent );
device->seek( 0 );
ok = true;
}
}

QgsPostgresConnPool::instance()->releaseConnection( conn );

return ok;
}


bool QgsPostgresProjectStorage::writeProject( const QString &uri, QIODevice *device, QgsReadWriteContext &context )
{
QgsPostgresProjectUri projectUri = parseUri( uri );
if ( !projectUri.valid )
{
context.pushMessage( "Invalid URI for PostgreSQL provider: " + uri, Qgis::Critical );
return false;
}

QgsPostgresConn *conn = QgsPostgresConnPool::instance()->acquireConnection( projectUri.connInfo.connectionInfo( false ) );

if ( !_projectsTableExists( *conn, projectUri.schemaName ) )
{
// try to create projects table
QString sql = QStringLiteral( "CREATE TABLE %1.qgis_projects(name TEXT PRIMARY KEY, metadata JSONB, content BYTEA)" ).arg( projectUri.schemaName );
QgsPostgresResult res( conn->PQexec( sql ) );
if ( res.PQresultStatus() != PGRES_COMMAND_OK )
{
QString errCause = QObject::tr( "Unable to save project. 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( projectUri.connInfo.username() );
context.pushMessage( errCause, Qgis::Critical );
QgsPostgresConnPool::instance()->releaseConnection( conn );
return false;
}
}

// read from device and write to the table
QByteArray content = device->readAll();

QString metadata = "{ \"last_modified\": 123 }"; // TODO: real metadata

// TODO: would be useful to have QByteArray version of PQexec() to avoid bytearray -> string -> bytearray conversion
QString sql( "INSERT INTO %1.qgis_projects VALUES (%2, %3, E'\\\\x" );
sql = sql.arg( QgsPostgresConn::quotedIdentifier( projectUri.schemaName ),
QgsPostgresConn::quotedValue( projectUri.projectName ),
QgsPostgresConn::quotedValue( metadata ) );
sql += QString::fromAscii( content.toHex() );
sql += "') ON CONFLICT (name) DO UPDATE SET content = EXCLUDED.content, metadata = EXCLUDED.metadata;";

QgsPostgresResult res( conn->PQexec( sql ) );
bool ok = res.PQresultStatus() == PGRES_COMMAND_OK;

QgsPostgresConnPool::instance()->releaseConnection( conn );

return ok;
}


bool QgsPostgresProjectStorage::removeProject( const QString &uri )
{
QgsPostgresProjectUri projectUri = parseUri( uri );
if ( !projectUri.valid )
return false;

QgsPostgresConn *conn = QgsPostgresConnPool::instance()->acquireConnection( projectUri.connInfo.connectionInfo( false ) );

bool removed = false;
if ( _projectsTableExists( *conn, projectUri.schemaName ) )
{
QString sql( QStringLiteral( "DELETE FROM %1.qgis_projects WHERE name = %2" ).arg( QgsPostgresConn::quotedIdentifier( projectUri.schemaName ), QgsPostgresConn::quotedValue( projectUri.projectName ) ) );
QgsPostgresResult res( conn->PQexec( sql ) );
removed = res.PQresultStatus() == PGRES_COMMAND_OK;
}

QgsPostgresConnPool::instance()->releaseConnection( conn );

return removed;
}


QgsPostgresProjectUri QgsPostgresProjectStorage::parseUri( const QString &uri )
{
QUrl u = QUrl::fromEncoded( uri.toUtf8() );
QUrlQuery urlQuery( u.query() );

QgsPostgresProjectUri postUri;
postUri.valid = u.isValid();

QString host = u.host();
QString port = u.port() != -1 ? QString::number( u.port() ) : QString();
QString username = u.userName();
QString password = u.password();
QgsDataSourceUri::SslMode sslMode = QgsDataSourceUri::SslPrefer;
QString authConfigId; // TODO: ?
QString dbName = urlQuery.queryItemValue( "dbname" );
postUri.connInfo.setConnection( host, port, dbName, username, password, sslMode, authConfigId );
// TODO: "service"

postUri.schemaName = urlQuery.queryItemValue( "schema" );
postUri.projectName = urlQuery.queryItemValue( "project" );
return postUri;
}
42 changes: 42 additions & 0 deletions src/providers/postgres/qgspostgresprojectstorage.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#ifndef QGSPOSTGRESPROJECTSTORAGE_H
#define QGSPOSTGRESPROJECTSTORAGE_H

#include "qgsprojectstorage.h"

#include "qgsdatasourceuri.h"

//! Stores information parsed from postgres project URI
typedef struct
{
bool valid;

QgsDataSourceUri connInfo; // using only the bits about connection info (server, port, username, password, service, ssl mode)

QString dbName;
QString schemaName;
QString projectName;

} QgsPostgresProjectUri;


//! Implements storage of QGIS projects inside a PostgreSQL table
class QgsPostgresProjectStorage : public QgsProjectStorage
{
public:
QgsPostgresProjectStorage();

virtual QString type() override { return QStringLiteral( "postgresql" ); }

virtual QStringList listProjects( const QString &uri ) override;

virtual bool readProject( const QString &uri, QIODevice *device, QgsReadWriteContext &context ) override;

virtual bool writeProject( const QString &uri, QIODevice *device, QgsReadWriteContext &context ) override;

virtual bool removeProject( const QString &uri ) override;

private:
static QgsPostgresProjectUri parseUri( const QString &uri );
};

#endif // QGSPOSTGRESPROJECTSTORAGE_H
15 changes: 15 additions & 0 deletions src/providers/postgres/qgspostgresprovider.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include "qgsgeometry.h"
#include "qgsmessageoutput.h"
#include "qgsmessagelog.h"
#include "qgsprojectstorageregistry.h"
#include "qgsrectangle.h"
#include "qgscoordinatereferencesystem.h"
#include "qgsxmlutils.h"
Expand All @@ -36,6 +37,7 @@
#include "qgspostgresfeatureiterator.h"
#include "qgspostgrestransaction.h"
#include "qgspostgreslistener.h"
#include "qgspostgresprojectstorage.h"
#include "qgslogger.h"
#include "qgsfeedback.h"
#include "qgssettings.h"
Expand Down Expand Up @@ -4911,8 +4913,21 @@ QGISEXTERN QgsTransaction *createTransaction( const QString &connString )
return new QgsPostgresTransaction( connString );
}


QgsPostgresProjectStorage *gProjectStorage = nullptr; // when not null it is owned by QgsApplication::projectStorageRegistry()

QGISEXTERN void initProvider()
{
Q_ASSERT( !gProjectStorage );
gProjectStorage = new QgsPostgresProjectStorage;
QgsApplication::projectStorageRegistry()->registerProjectStorage( gProjectStorage ); // takes ownership
}

QGISEXTERN void cleanupProvider()
{
QgsApplication::projectStorageRegistry()->unregisterProjectStorage( gProjectStorage ); // destroys the object
gProjectStorage = nullptr;

QgsPostgresConnPool::cleanupInstance();
}

Expand Down
1 change: 1 addition & 0 deletions tests/src/python/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ IF (ENABLE_PGTEST)
ADD_PYTHON_TEST(PyQgsPostgresProvider test_provider_postgres.py)
ADD_PYTHON_TEST(PyQgsRelationEditWidget test_qgsrelationeditwidget.py)
ADD_PYTHON_TEST(PyQgsVectorLayerTools test_qgsvectorlayertools.py)
ADD_PYTHON_TEST(PyQgsProjectStoragePostgres test_project_storage_postgres.py)
ADD_PYTHON_TEST(PyQgsAuthManagerPKIPostgresTest test_authmanager_pki_postgres.py)
ADD_PYTHON_TEST(PyQgsAuthManagerPasswordPostgresTest test_authmanager_password_postgres.py)
ADD_PYTHON_TEST(PyQgsAuthManagerOgrPostgresTest test_authmanager_ogr_postgres.py)
Expand Down
Loading

0 comments on commit 0f5ea53

Please sign in to comment.