diff --git a/python/core/qgsproject.sip b/python/core/qgsproject.sip
index 6a24cb39309..6df43302744 100644
--- a/python/core/qgsproject.sip
+++ b/python/core/qgsproject.sip
@@ -150,7 +150,7 @@ class QgsProject : QObject
keys would be the familiar QSettings-like '/' delimited entries, implying
a hierarchy of keys and corresponding values
- @note The key string must include '/'s. E.g., "/foo" not "foo".
+ @note The key string must be valid xml tag names in order to be saved to the file.
*/
//@{
//! @note available in python bindings as writeEntryBool
@@ -167,8 +167,6 @@ class QgsProject : QObject
keys would be the familiar QSettings-like '/' delimited entries,
implying a hierarchy of keys and corresponding values
-
- @note The key string must include '/'s. E.g., "/foo" not "foo".
*/
//@{
QStringList readListEntry( const QString & scope, const QString & key, const QStringList& def = QStringList(), bool *ok = 0 ) const;
diff --git a/src/core/qgsproject.cpp b/src/core/qgsproject.cpp
index bb5c770230c..d6156e6068f 100644
--- a/src/core/qgsproject.cpp
+++ b/src/core/qgsproject.cpp
@@ -27,6 +27,7 @@
#include "qgslayertreeregistrybridge.h"
#include "qgslogger.h"
#include "qgsmaplayerregistry.h"
+#include "qgsmessagelog.h"
#include "qgspluginlayer.h"
#include "qgspluginlayerregistry.h"
#include "qgsprojectfiletransform.h"
@@ -66,6 +67,24 @@ QStringList makeKeyTokens_( QString const &scope, QString const &key )
// be sure to include the canonical root node
keyTokens.push_front( "properties" );
+ //check validy of keys since an unvalid xml name will will be dropped upon saving the xml file. If not valid, we print a message to the console.
+ for (int i = 0; i < keyTokens.size(); ++i){
+ QString keyToken = keyTokens.at(i);
+
+ //invalid chars in XML are found at http://www.w3.org/TR/REC-xml/#NT-NameChar
+ //note : it seems \x10000-\xEFFFF is valid, but it when added to the regexp, a lot of unwanted characters remain
+ QString nameCharRegexp = QString( "[^:A-Z_a-z\\xC0-\\xD6\\xD8-\\xF6\\xF8-\\x2FF\\x370-\\x37D\\x37F-\\x1FFF\\x200C-\\x200D\\x2070-\\x218F\\x2C00-\\x2FEF\\x3001-\\xD7FF\\xF900-\\xFDCF\\xFDF0-\\xFFFD\\-\\.0-9\\xB7\\x0300-\\x036F\\x203F-\\x2040]" );
+ QString nameStartCharRegexp = QString( "^[^:A-Z_a-z\\xC0-\\xD6\\xD8-\\xF6\\xF8-\\x2FF\\x370-\\x37D\\x37F-\\x1FFF\\x200C-\\x200D\\x2070-\\x218F\\x2C00-\\x2FEF\\x3001-\\xD7FF\\xF900-\\xFDCF\\xFDF0-\\xFFFD]" );
+
+ if( keyToken.contains( QRegExp(nameCharRegexp) ) || keyToken.contains( QRegExp(nameStartCharRegexp) ) ){
+
+ QString errorString = QObject::tr("Entry token invalid : '%1'. The token will not be saved to file.").arg(keyToken);
+ QgsMessageLog::logMessage( errorString, QString::null, QgsMessageLog::CRITICAL );
+
+ }
+
+ }
+
return keyTokens;
} // makeKeyTokens_
diff --git a/src/core/qgsproject.h b/src/core/qgsproject.h
index d7f27de2181..02273ce1f93 100644
--- a/src/core/qgsproject.h
+++ b/src/core/qgsproject.h
@@ -197,7 +197,7 @@ class CORE_EXPORT QgsProject : public QObject
keys would be the familiar QSettings-like '/' delimited entries, implying
a hierarchy of keys and corresponding values
- @note The key string must include '/'s. E.g., "/foo" not "foo".
+ @note The key string must be valid xml tag names in order to be saved to the file.
*/
//@{
//! @note available in python bindings as writeEntryBool
@@ -214,8 +214,6 @@ class CORE_EXPORT QgsProject : public QObject
keys would be the familiar QSettings-like '/' delimited entries,
implying a hierarchy of keys and corresponding values
-
- @note The key string must include '/'s. E.g., "/foo" not "foo".
*/
//@{
QStringList readListEntry( const QString & scope, const QString & key, const QStringList& def = QStringList(), bool *ok = 0 ) const;
diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt
index 1af4d141f04..74777b7145a 100644
--- a/tests/src/python/CMakeLists.txt
+++ b/tests/src/python/CMakeLists.txt
@@ -30,6 +30,7 @@ ADD_PYTHON_TEST(PyQgsDistanceArea test_qgsdistancearea.py)
ADD_PYTHON_TEST(PyQgsEditWidgets test_qgseditwidgets.py)
ADD_PYTHON_TEST(PyQgsExpression test_qgsexpression.py)
ADD_PYTHON_TEST(PyQgsFeature test_qgsfeature.py)
+ADD_PYTHON_TEST(PyQgsProject test_qgsproject.py)
ADD_PYTHON_TEST(PyQgsFeatureIterator test_qgsfeatureiterator.py)
ADD_PYTHON_TEST(PyQgsField test_qgsfield.py)
ADD_PYTHON_TEST(PyQgsFontUtils test_qgsfontutils.py)
diff --git a/tests/src/python/test_qgsproject.py b/tests/src/python/test_qgsproject.py
new file mode 100644
index 00000000000..5dcbc2245ed
--- /dev/null
+++ b/tests/src/python/test_qgsproject.py
@@ -0,0 +1,113 @@
+# -*- coding: utf-8 -*-
+"""QGIS Unit tests for QgsProject.
+
+.. 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__ = 'Sebastian Dietrich'
+__date__ = '19/11/2015'
+__copyright__ = 'Copyright 2015, The QGIS Project'
+# This will get replaced with a git SHA1 when you do a git archive
+__revision__ = '$Format:%H$'
+
+import qgis
+
+from qgis.core import QgsProject, QgsMessageLog
+
+from utilities import getQgisTestApp, TestCase, unittest
+
+QGISAPP, CANVAS, IFACE, PARENT = getQgisTestApp()
+
+
+class TestQgsProject(TestCase):
+
+ def __init__(self, methodName):
+ """Run once on class initialisation."""
+ unittest.TestCase.__init__(self, methodName)
+ self.messageCaught = False
+
+ def test_makeKeyTokens_(self):
+ # see http://www.w3.org/TR/REC-xml/#d0e804 for a list of valid characters
+
+ invalidTokens = []
+ validTokens = []
+
+ # all test tokens will be generated by prepending or inserting characters to this token
+ validBase = u"valid";
+
+ # some invalid characters, not allowed anywhere in a token
+ # note that '/' must not be added here because it is taken as a separator by makeKeyTokens_()
+ invalidChars = u"+*,;<>|!$%()=?#\x01";
+
+ # generate the characters that are allowed at the start of a token (and at every other position)
+ validStartChars = u":_";
+ charRanges = [
+ (ord(u'a'), ord(u'z')),
+ (ord(u'A'), ord(u'Z')),
+ (0x00F8, 0x02FF),
+ (0x0370, 0x037D),
+ (0x037F, 0x1FFF),
+ (0x200C, 0x200D),
+ (0x2070, 0x218F),
+ (0x2C00, 0x2FEF),
+ (0x3001, 0xD7FF),
+ (0xF900, 0xFDCF),
+ (0xFDF0, 0xFFFD),
+ #(0x10000, 0xEFFFF), while actually valid, these are not yet accepted by makeKeyTokens_()
+ ]
+ for r in charRanges:
+ for c in range(r[0], r[1]):
+ validStartChars += unichr(c)
+
+ # generate the characters that are only allowed inside a token, not at the start
+ validInlineChars = u"-.\xB7";
+ charRanges = [
+ (ord(u'0'), ord(u'9')),
+ (0x0300, 0x036F),
+ (0x203F, 0x2040),
+ ]
+ for r in charRanges:
+ for c in range(r[0], r[1]):
+ validInlineChars += unichr(c)
+
+ # test forbidden start characters
+ for c in invalidChars + validInlineChars:
+ invalidTokens.append(c + validBase)
+
+ # test forbidden inline characters
+ for c in invalidChars:
+ invalidTokens.append(validBase[:4] + c + validBase[4:])
+
+ # test each allowed start character
+ for c in validStartChars:
+ validTokens.append(c + validBase)
+
+ # test each allowed inline character
+ for c in validInlineChars:
+ validTokens.append(validBase[:4] + c + validBase[4:]);
+
+ logger = QgsMessageLog.instance()
+ logger.messageReceived.connect(self.catchMessage)
+ prj = QgsProject.instance()
+
+ for token in validTokens:
+ self.messageCaught = False
+ prj.readEntry("test", token)
+ myMessage = "valid token '%s' not accepted" % (token)
+ assert not self.messageCaught, myMessage
+
+ for token in invalidTokens:
+ self.messageCaught = False
+ prj.readEntry("test", token)
+ myMessage = "invalid token '%s' accepted" % (token)
+ assert self.messageCaught, myMessage
+
+ logger.messageReceived.disconnect(self.catchMessage)
+
+ def catchMessage(self):
+ self.messageCaught = True
+
+if __name__ == '__main__':
+ unittest.main()