[expressions] Make file function (such as base_file_name) gracefully

handle map layer values

These are treated as the file path to the layer.

E.g.

    base_file_name(@some_variable_which_is_a_layer)

will return the base file name of the layer's source
This commit is contained in:
Nyall Dawson 2022-02-07 11:41:29 +10:00
parent 45ca76cede
commit 2bd18332aa
14 changed files with 134 additions and 23 deletions

View File

@ -5,7 +5,7 @@
"description": "Returns the base name of the file without the directory or file suffix.",
"arguments": [{
"arg": "path",
"description": "a file path"
"description": "a file path or a map layer value. If a map layer layer value is specified then the file source of the layer will be used."
}],
"examples": [{
"expression": "base_file_name('/home/qgis/data/country_boundaries.shp')",

View File

@ -5,7 +5,7 @@
"description": "Retrieves exif tag values from an image file.",
"arguments": [{
"arg": "path",
"description": "An image file path."
"description": "An image file path or a map layer value. If a map layer layer value is specified then the file source of the layer will be used."
}, {
"arg": "tag",
"optional": true,

View File

@ -5,7 +5,7 @@
"description": "Creates a point geometry from the exif geotags of an image file.",
"arguments": [{
"arg": "path",
"description": "An image file path."
"description": "An image file path or a map layer value. If a map layer layer value is specified then the file source of the layer will be used."
}],
"examples": [{
"expression": "geom_to_wkt(exif_geotag('/my/photo.jpg'))",

View File

@ -5,7 +5,7 @@
"description": "Returns true if a file path exists.",
"arguments": [{
"arg": "path",
"description": "a file path"
"description": "a file path or a map layer value. If a map layer layer value is specified then the file source of the layer will be used."
}],
"examples": [{
"expression": "file_exists('/home/qgis/data/country_boundaries.shp')",

View File

@ -5,7 +5,7 @@
"description": "Returns the name of a file (including the file extension), excluding the directory.",
"arguments": [{
"arg": "path",
"description": "a file path"
"description": "a file path or a map layer value. If a map layer layer value is specified then the file source of the layer will be used."
}],
"examples": [{
"expression": "file_name('/home/qgis/data/country_boundaries.shp')",

View File

@ -5,7 +5,7 @@
"description": "Returns the directory component of a file path. This does not include the file name.",
"arguments": [{
"arg": "path",
"description": "a file path"
"description": "a file path or a map layer value. If a map layer layer value is specified then the file source of the layer will be used."
}],
"examples": [{
"expression": "file_path('/home/qgis/data/country_boundaries.shp')",

View File

@ -5,7 +5,7 @@
"description": "Returns the size (in bytes) of a file.",
"arguments": [{
"arg": "path",
"description": "a file path"
"description": "a file path or a map layer value. If a map layer layer value is specified then the file source of the layer will be used."
}],
"examples": [{
"expression": "file_size('/home/qgis/data/country_boundaries.geojson')",

View File

@ -5,7 +5,7 @@
"description": "Returns the file suffix (extension) from a file path.",
"arguments": [{
"arg": "path",
"description": "a file path"
"description": "a file path or a map layer value. If a map layer layer value is specified then the file source of the layer will be used."
}],
"examples": [{
"expression": "file_suffix('/home/qgis/data/country_boundaries.shp')",

View File

@ -5,7 +5,7 @@
"description": "Returns true if a path corresponds to a directory.",
"arguments": [{
"arg": "path",
"description": "a file path"
"description": "a file path or a map layer value. If a map layer layer value is specified then the file source of the layer will be used."
}],
"examples": [{
"expression": "is_directory('/home/qgis/data/country_boundaries.shp')",

View File

@ -5,7 +5,7 @@
"description": "Returns true if a path corresponds to a file.",
"arguments": [{
"arg": "path",
"description": "a file path"
"description": "a file path or a map layer value. If a map layer layer value is specified then the file source of the layer will be used."
}],
"examples": [{
"expression": "is_file('/home/qgis/data/country_boundaries.shp')",

View File

@ -2464,14 +2464,24 @@ static QVariant fcnDateTimeFromEpoch( const QVariantList &values, const QgsExpre
static QVariant fcnExif( const QVariantList &values, const QgsExpressionContext *, QgsExpression *parent, const QgsExpressionNodeFunction * )
{
QString filepath = QgsExpressionUtils::getStringValue( values.at( 0 ), parent );
const QString filepath = QgsExpressionUtils::getFilePathValue( values.at( 0 ), parent );
if ( parent->hasEvalError() )
{
parent->setEvalErrorString( QObject::tr( "Function `exif` requires a value which represents a possible file path" ) );
return QVariant();
}
QString tag = QgsExpressionUtils::getStringValue( values.at( 1 ), parent );
return !tag.isNull() ? QgsExifTools::readTag( filepath, tag ) : QVariant( QgsExifTools::readTags( filepath ) );
}
static QVariant fcnExifGeoTag( const QVariantList &values, const QgsExpressionContext *, QgsExpression *parent, const QgsExpressionNodeFunction * )
{
QString filepath = QgsExpressionUtils::getStringValue( values.at( 0 ), parent );
const QString filepath = QgsExpressionUtils::getFilePathValue( values.at( 0 ), parent );
if ( parent->hasEvalError() )
{
parent->setEvalErrorString( QObject::tr( "Function `exif_geotag` requires a value which represents a possible file path" ) );
return QVariant();
}
bool ok;
return QVariant::fromValue( QgsGeometry( new QgsPoint( QgsExifTools::getGeoTag( filepath, ok ) ) ) );
}
@ -6515,53 +6525,91 @@ static QVariant fcnEnvVar( const QVariantList &values, const QgsExpressionContex
static QVariant fcnBaseFileName( const QVariantList &values, const QgsExpressionContext *, QgsExpression *parent, const QgsExpressionNodeFunction * )
{
const QString file = QgsExpressionUtils::getStringValue( values.at( 0 ), parent );
const QString file = QgsExpressionUtils::getFilePathValue( values.at( 0 ), parent );
if ( parent->hasEvalError() )
{
parent->setEvalErrorString( QObject::tr( "Function `base_file_name` requires a value which represents a possible file path" ) );
return QVariant();
}
return QFileInfo( file ).completeBaseName();
}
static QVariant fcnFileSuffix( const QVariantList &values, const QgsExpressionContext *, QgsExpression *parent, const QgsExpressionNodeFunction * )
{
const QString file = QgsExpressionUtils::getStringValue( values.at( 0 ), parent );
const QString file = QgsExpressionUtils::getFilePathValue( values.at( 0 ), parent );
if ( parent->hasEvalError() )
{
parent->setEvalErrorString( QObject::tr( "Function `file_suffix` requires a value which represents a possible file path" ) );
return QVariant();
}
return QFileInfo( file ).completeSuffix();
}
static QVariant fcnFileExists( const QVariantList &values, const QgsExpressionContext *, QgsExpression *parent, const QgsExpressionNodeFunction * )
{
const QString file = QgsExpressionUtils::getStringValue( values.at( 0 ), parent );
return QFileInfo::exists( file );
const QString file = QgsExpressionUtils::getFilePathValue( values.at( 0 ), parent );
if ( parent->hasEvalError() )
{
parent->setEvalErrorString( QObject::tr( "Function `file_exists` requires a value which represents a possible file path" ) );
return QVariant();
} return QFileInfo::exists( file );
}
static QVariant fcnFileName( const QVariantList &values, const QgsExpressionContext *, QgsExpression *parent, const QgsExpressionNodeFunction * )
{
const QString file = QgsExpressionUtils::getStringValue( values.at( 0 ), parent );
return QFileInfo( file ).fileName();
const QString file = QgsExpressionUtils::getFilePathValue( values.at( 0 ), parent );
if ( parent->hasEvalError() )
{
parent->setEvalErrorString( QObject::tr( "Function `file_name` requires a value which represents a possible file path" ) );
return QVariant();
} return QFileInfo( file ).fileName();
}
static QVariant fcnPathIsFile( const QVariantList &values, const QgsExpressionContext *, QgsExpression *parent, const QgsExpressionNodeFunction * )
{
const QString file = QgsExpressionUtils::getStringValue( values.at( 0 ), parent );
const QString file = QgsExpressionUtils::getFilePathValue( values.at( 0 ), parent );
if ( parent->hasEvalError() )
{
parent->setEvalErrorString( QObject::tr( "Function `is_file` requires a value which represents a possible file path" ) );
return QVariant();
}
return QFileInfo( file ).isFile();
}
static QVariant fcnPathIsDir( const QVariantList &values, const QgsExpressionContext *, QgsExpression *parent, const QgsExpressionNodeFunction * )
{
const QString file = QgsExpressionUtils::getStringValue( values.at( 0 ), parent );
const QString file = QgsExpressionUtils::getFilePathValue( values.at( 0 ), parent );
if ( parent->hasEvalError() )
{
parent->setEvalErrorString( QObject::tr( "Function `is_directory` requires a value which represents a possible file path" ) );
return QVariant();
}
return QFileInfo( file ).isDir();
}
static QVariant fcnFilePath( const QVariantList &values, const QgsExpressionContext *, QgsExpression *parent, const QgsExpressionNodeFunction * )
{
const QString file = QgsExpressionUtils::getStringValue( values.at( 0 ), parent );
const QString file = QgsExpressionUtils::getFilePathValue( values.at( 0 ), parent );
if ( parent->hasEvalError() )
{
parent->setEvalErrorString( QObject::tr( "Function `file_path` requires a value which represents a possible file path" ) );
return QVariant();
}
return QDir::toNativeSeparators( QFileInfo( file ).path() );
}
static QVariant fcnFileSize( const QVariantList &values, const QgsExpressionContext *, QgsExpression *parent, const QgsExpressionNodeFunction * )
{
const QString file = QgsExpressionUtils::getStringValue( values.at( 0 ), parent );
const QString file = QgsExpressionUtils::getFilePathValue( values.at( 0 ), parent );
if ( parent->hasEvalError() )
{
parent->setEvalErrorString( QObject::tr( "Function `file_size` requires a value which represents a possible file path" ) );
return QVariant();
}
return QFileInfo( file ).size();
}
static QVariant fcnHash( const QString str, const QCryptographicHash::Algorithm algorithm )
static QVariant fcnHash( const QString &str, const QCryptographicHash::Algorithm algorithm )
{
return QString( QCryptographicHash::hash( str.toUtf8(), algorithm ).toHex() );
}

View File

@ -17,6 +17,7 @@
#include "qgsexpressionnode.h"
#include "qgsvectorlayer.h"
#include "qgscolorrampimpl.h"
#include "qgsproviderregistry.h"
///@cond PRIVATE
@ -49,6 +50,26 @@ QgsGradientColorRamp QgsExpressionUtils::getRamp( const QVariant &value, QgsExpr
return QgsGradientColorRamp();
}
QString QgsExpressionUtils::getFilePathValue( const QVariant &value, QgsExpression *parent )
{
// if it's a map layer, return the file path of that layer...
QString res;
if ( QgsMapLayer *layer = getMapLayer( value, parent ) )
{
const QVariantMap parts = QgsProviderRegistry::instance()->decodeUri( layer->providerType(), layer->source() );
res = parts.value( QStringLiteral( "path" ) ).toString();
}
if ( res.isEmpty() )
res = value.toString();
if ( res.isEmpty() && !value.isNull() )
{
parent->setEvalErrorString( QObject::tr( "Cannot convert value to a file path" ) );
}
return res;
}
///@endcond
std::tuple<QVariant::Type, int> QgsExpressionUtils::determineResultType( const QString &expression, const QgsVectorLayer *layer, QgsFeatureRequest request, QgsExpressionContext context, bool *foundFeatures )

View File

@ -419,6 +419,13 @@ class CORE_EXPORT QgsExpressionUtils
return qobject_cast<QgsMeshLayer *>( getMapLayer( value, e ) );
}
/**
* Tries to convert a \a value to a file path.
*
* \since QGIS 3.24
*/
static QString getFilePathValue( const QVariant &value, QgsExpression *parent );
static QVariantList getListValue( const QVariant &value, QgsExpression *parent )
{
if ( value.type() == QVariant::List || value.type() == QVariant::StringList )

View File

@ -1990,35 +1990,43 @@ class TestQgsExpression: public QObject
QTest::newRow( "base_file_name(NULL)" ) << QStringLiteral( "base_file_name(NULL)" ) << false << QVariant();
QTest::newRow( "base_file_name('/home/qgis/test.qgs')" ) << QStringLiteral( "base_file_name('/home/qgis/test.qgs')" ) << false << QVariant( "test" );
QTest::newRow( "base_file_name(points.shp)" ) << QStringLiteral( "base_file_name('%1/points.shp')" ).arg( TEST_DATA_DIR ) << false << QVariant( "points" );
QTest::newRow( "base_file_name(map layer)" ) << QStringLiteral( "base_file_name('%1')" ).arg( mPointsLayer->id() ) << false << QVariant( "points" );
QTest::newRow( "file_exists(NULL)" ) << QStringLiteral( "file_exists(NULL)" ) << false << QVariant();
QTest::newRow( "file_exists('/home/qgis/test.qgs')" ) << QStringLiteral( "file_exists('/home/qgis/test.qgs')" ) << false << QVariant( false );
QTest::newRow( "file_exists(points.shp)" ) << QStringLiteral( "file_exists('%1/points.shp')" ).arg( TEST_DATA_DIR ) << false << QVariant( true );
QTest::newRow( "file_exists(map layer)" ) << QStringLiteral( "file_exists('%1')" ).arg( mPointsLayer->id() ) << false << QVariant( true );
QTest::newRow( "file_name(5)" ) << QStringLiteral( "file_name(5)" ) << false << QVariant( "5" );
QTest::newRow( "file_name(NULL)" ) << QStringLiteral( "file_name(NULL)" ) << false << QVariant();
QTest::newRow( "file_name('/home/qgis/test.qgs')" ) << QStringLiteral( "file_name('/home/qgis/test.qgs')" ) << false << QVariant( "test.qgs" );
QTest::newRow( "file_name(points.shp)" ) << QStringLiteral( "file_name('%1/points.shp')" ).arg( TEST_DATA_DIR ) << false << QVariant( "points.shp" );
QTest::newRow( "file_name(map layer)" ) << QStringLiteral( "file_name('%1')" ).arg( mPointsLayer->id() ) << false << QVariant( "points.shp" );
QTest::newRow( "file_path(5)" ) << QStringLiteral( "file_path(5)" ) << false << QVariant( "." );
QTest::newRow( "file_path(NULL)" ) << QStringLiteral( "file_path(NULL)" ) << false << QVariant();
QTest::newRow( "file_path('/home/qgis/test.qgs')" ) << QStringLiteral( "file_path('/home/qgis/test.qgs')" ) << false << QVariant( "/home/qgis" );
QTest::newRow( "file_path(points.shp)" ) << QStringLiteral( "file_path('%1/points.shp')" ).arg( TEST_DATA_DIR ) << false << QVariant( TEST_DATA_DIR );
QTest::newRow( "file_path(map layer)" ) << QStringLiteral( "file_path('%1')" ).arg( mPointsLayer->id() ) << false << QVariant( TEST_DATA_DIR );
QTest::newRow( "file_size(5)" ) << QStringLiteral( "file_size(5)" ) << false << QVariant( 0LL );
QTest::newRow( "file_size(NULL)" ) << QStringLiteral( "file_size(NULL)" ) << false << QVariant();
QTest::newRow( "file_size('/home/qgis/test.qgs')" ) << QStringLiteral( "file_size('/home/qgis/test.qgs')" ) << false << QVariant( 0LL );
QTest::newRow( "file_size(points.shp)" ) << QStringLiteral( "file_size('%1/points.shp')" ).arg( TEST_DATA_DIR ) << false << QVariant( 576LL );
QTest::newRow( "file_size(map layer)" ) << QStringLiteral( "file_size('%1')" ).arg( mPointsLayer->id() ) << false << QVariant( 576LL );
QTest::newRow( "file_suffix(5)" ) << QStringLiteral( "file_suffix(5)" ) << false << QVariant( "" );
QTest::newRow( "file_suffix(NULL)" ) << QStringLiteral( "file_suffix(NULL)" ) << false << QVariant();
QTest::newRow( "file_suffix('/home/qgis/test.qgs')" ) << QStringLiteral( "file_suffix('/home/qgis/test.qgs')" ) << false << QVariant( "qgs" );
QTest::newRow( "file_suffix(points.shp)" ) << QStringLiteral( "file_suffix('%1/points.shp')" ).arg( TEST_DATA_DIR ) << false << QVariant( "shp" );
QTest::newRow( "file_suffix(map layer)" ) << QStringLiteral( "file_suffix('%1')" ).arg( mPointsLayer->id() ) << false << QVariant( "shp" );
QTest::newRow( "is_directory(5)" ) << QStringLiteral( "is_directory(5)" ) << false << QVariant( false );
QTest::newRow( "is_directory(NULL)" ) << QStringLiteral( "is_directory(NULL)" ) << false << QVariant();
QTest::newRow( "is_directory('/home/qgis/test.qgs')" ) << QStringLiteral( "is_directory('/home/qgis/test.qgs')" ) << false << QVariant( false );
QTest::newRow( "is_directory(points.shp)" ) << QStringLiteral( "is_directory('%1/points.shp')" ).arg( TEST_DATA_DIR ) << false << QVariant( false );
QTest::newRow( "is_directory(valid)" ) << QStringLiteral( "is_directory('%1')" ).arg( TEST_DATA_DIR ) << false << QVariant( true );
QTest::newRow( "is_directory(map layer)" ) << QStringLiteral( "is_directory('%1')" ).arg( mPointsLayer->id() ) << false << QVariant( false );
QTest::newRow( "is_file(5)" ) << QStringLiteral( "is_file(5)" ) << false << QVariant( false );
QTest::newRow( "is_file(NULL)" ) << QStringLiteral( "is_file(NULL)" ) << false << QVariant();
QTest::newRow( "is_file('/home/qgis/test.qgs')" ) << QStringLiteral( "is_file('/home/qgis/test.qgs')" ) << false << QVariant( false );
QTest::newRow( "is_file(points.shp)" ) << QStringLiteral( "is_file('%1/points.shp')" ).arg( TEST_DATA_DIR ) << false << QVariant( true );
QTest::newRow( "is_file(valid)" ) << QStringLiteral( "is_file('%1')" ).arg( TEST_DATA_DIR ) << false << QVariant( false );
QTest::newRow( "is_file(map layer)" ) << QStringLiteral( "is_file('%1')" ).arg( mPointsLayer->id() ) << false << QVariant( true );
// hash functions
QTest::newRow( "md5(NULL)" ) << QStringLiteral( "md5(NULL)" ) << false << QVariant();
@ -4297,6 +4305,33 @@ class TestQgsExpression: public QObject
#endif
}
void testGetFilePathValue()
{
QgsExpression exp;
// NULL value
QString path = QgsExpressionUtils::getFilePathValue( QVariant(), &exp );
QVERIFY( path.isEmpty() );
QVERIFY( !exp.hasEvalError() );
// value which CANNOT be a file path
path = QgsExpressionUtils::getFilePathValue( QVariant::fromValue( QgsGeometry() ), &exp );
QVERIFY( path.isEmpty() );
QVERIFY( exp.hasEvalError() );
QCOMPARE( exp.evalErrorString(), QStringLiteral( "Cannot convert value to a file path" ) );
// good value
exp = QgsExpression();
path = QgsExpressionUtils::getFilePathValue( QVariant::fromValue( QStringLiteral( "/home/me/mine.txt" ) ), &exp );
QCOMPARE( path, QStringLiteral( "/home/me/mine.txt" ) );
QVERIFY( !exp.hasEvalError() );
// with map layer pointer -- should use layer path
exp = QgsExpression();
path = QgsExpressionUtils::getFilePathValue( QVariant::fromValue( mPointsLayer ), &exp );
QCOMPARE( path, QStringLiteral( TEST_DATA_DIR ) + QStringLiteral( "/points.shp" ) );
QVERIFY( !exp.hasEvalError() );
}
void test_env()
{
QgsExpressionContext context;