From 230cf9e970a56be5dd22a8a32b4600ffce0628f5 Mon Sep 17 00:00:00 2001 From: Paulchen-Panther Date: Wed, 20 Jul 2016 17:16:06 +0200 Subject: [PATCH] Rewrite JSONSchemaChecker to QT (#128) * Add files via upload * Update CMakeLists.txt * Add files via upload * Update TestConfigFile.cpp * Update TestConfigFile.cpp * Update TestConfigFile.cpp * Update QJsonFactory.h --- include/utils/jsonschema/QJsonFactory.h | 88 ++++ include/utils/jsonschema/QJsonSchemaChecker.h | 190 +++++++++ libsrc/utils/CMakeLists.txt | 4 + .../utils/jsonschema/QJsonSchemaChecker.cpp | 375 ++++++++++++++++++ test/TestConfigFile.cpp | 87 ++-- 5 files changed, 721 insertions(+), 23 deletions(-) create mode 100644 include/utils/jsonschema/QJsonFactory.h create mode 100644 include/utils/jsonschema/QJsonSchemaChecker.h create mode 100644 libsrc/utils/jsonschema/QJsonSchemaChecker.cpp diff --git a/include/utils/jsonschema/QJsonFactory.h b/include/utils/jsonschema/QJsonFactory.h new file mode 100644 index 00000000..0234ee96 --- /dev/null +++ b/include/utils/jsonschema/QJsonFactory.h @@ -0,0 +1,88 @@ +#pragma once + +#include +#include +#include +#include + +// JSON-Schema includes +#include + +#include +#include +#include +#include +#include + +class QJsonFactory +{ +public: + + static int load(const QString& schema, const QString& config, QJsonObject& json) + { + // Load the schema and the config trees + QJsonObject schemaTree = readJson(schema); + QJsonObject configTree = readJson(config); + + // create the validator + QJsonSchemaChecker schemaChecker; + schemaChecker.setSchema(schemaTree); + + bool valid = schemaChecker.validate(configTree); + + for (std::list::const_iterator i = schemaChecker.getMessages().begin(); i != schemaChecker.getMessages().end(); ++i) + { + std::cout << *i << std::endl; + } + + if (!valid) + { + std::cerr << "Validation failed for configuration file: " << config.toStdString() << std::endl; + return -3; + } + + json = configTree; + return 0; + } + + static QJsonObject readJson(const QString& path) + { + QFile file(path); + QJsonParseError error; + + if (!file.open(QIODevice::ReadOnly)) + { + std::stringstream sstream; + sstream << "Configuration file not found: " << file.errorString().toStdString(); + throw std::runtime_error(sstream.str()); + } + + QString config = QString(file.readAll()); + config.remove(QRegularExpression("([^:]?\\/\\/.*)")); + + QJsonDocument doc = QJsonDocument::fromJson(config.toUtf8(), &error); + + if (error.error != QJsonParseError::NoError) + { + // report to the user the failure and their locations in the document. + int errorLine(0), errorColumn(0); + + for( int i=0, count=qMin( error.offset,config.size()); i +#include + +#include +#include +#include +#include +#include + +/// JsonSchemaChecker is a very basic implementation of json schema. +/// The json schema definition draft can be found at +/// http://tools.ietf.org/html/draft-zyp-json-schema-03 +/// +/// The following keywords are supported: +/// - type +/// - required +/// - properties +/// - items +/// - enum +/// - minimum +/// - maximum +/// - addtionalProperties +/// - minItems +/// - maxItems + +class QJsonSchemaChecker +{ +public: + QJsonSchemaChecker(); + virtual ~QJsonSchemaChecker(); + + /// + /// @param schema The schema to use + /// @return true upon succes + /// + bool setSchema(const QJsonObject & schema); + + /// + /// @brief Validate a JSON structure + /// @param value The JSON value to check + /// @return true when the arguments is valid according to the schema + /// + bool validate(const QJsonObject & value); + + /// + /// @return A list of error messages + /// + const std::list & getMessages() const; + +private: + /// + /// Validates a json-value against a given schema. Results are stored in the members of this + /// class (_error & _messages) + /// + /// @param[in] value The value to validate + /// @param[in] schema The schema against which the value is validated + /// + void validate(const QJsonValue &value, const QJsonObject & schema); + + /// + /// Adds the given message to the message-queue (with reference to current line-number) + /// + /// @param[in] message The message to add to the queue + /// + void setMessage(const std::string & message); + + /// + /// Retrieves all references from the json-value as specified by the schema + /// + /// @param[in] value The json-value + /// @param[in] schema The schema + /// + void collectDependencies(const QJsonValue & value, const QJsonObject &schema); + +private: + // attribute check functions + /// + /// Checks if the given value is of the specified type. If the type does not match _error is set + /// to true and an error-message is added to the message-queue + /// + /// @param[in] value The given value + /// @param[in] schema The specified type (as json-value) + /// + void checkType(const QJsonValue & value, const QJsonValue & schema); + + /// + /// Checks is required properties of an json-object exist and if all properties are of the + /// correct format. If this is not the case _error is set to true and an error-message is added + /// to the message-queue. + /// + /// @param[in] value The given json-object + /// @param[in] schema The schema of the json-object + /// + void checkProperties(const QJsonObject & value, const QJsonObject & schema); + + /// + /// Verifies the additional configured properties of an json-object. If this is not the case + /// _error is set to true and an error-message is added to the message-queue. + /// + /// @param value The given json-object + /// @param schema The schema for the json-object + /// @param ignoredProperties The properties that were ignored + /// + void checkAdditionalProperties(const QJsonObject & value, const QJsonValue & schema, const QStringList & ignoredProperties); + + /// + /// Checks if references are configued and used correctly. If this is not the case _error is set + /// to true and an error-message is added to the message-queue. + /// + /// @param value The given json-object + /// @param schemaLink The schema of the json-object + /// + void checkDependencies(const QJsonValue & value, const QJsonValue & schemaLink); + + /// + /// Checks if the given value is larger or equal to the specified value. If this is not the case + /// _error is set to true and an error-message is added to the message-queue. + /// + /// @param[in] value The given value + /// @param[in] schema The minimum value (as json-value) + /// + void checkMinimum(const QJsonValue & value, const QJsonValue & schema); + + /// + /// Checks if the given value is smaller or equal to the specified value. If this is not the + /// case _error is set to true and an error-message is added to the message-queue. + /// + /// @param[in] value The given value + /// @param[in] schema The maximum value (as json-value) + /// + void checkMaximum(const QJsonValue & value, const QJsonValue & schema); + + /// + /// Validates all the items of an array. + /// + /// @param value The json-array + /// @param schema The schema for the items in the array + /// + void checkItems(const QJsonValue & value, const QJsonObject & schema); + + /// + /// Checks if a given array has at least a minimum number of items. If this is not the case + /// _error is set to true and an error-message is added to the message-queue. + /// + /// @param value The json-array + /// @param schema The minimum size specification (as json-value) + /// + void checkMinItems(const QJsonValue & value, const QJsonValue & schema); + + /// + /// Checks if a given array has at most a maximum number of items. If this is not the case + /// _error is set to true and an error-message is added to the message-queue. + /// + /// @param value The json-array + /// @param schema The maximum size specification (as json-value) + /// + void checkMaxItems(const QJsonValue & value, const QJsonValue & schema); + + /// + /// Checks if a given array contains only unique items. If this is not the case + /// _error is set to true and an error-message is added to the message-queue. + /// + /// @param value The json-array + /// @param schema Bool to enable the check (as json-value) + /// + void checkUniqueItems(const QJsonValue & value, const QJsonValue & schema); + + /// + /// Checks if an enum value is actually a valid value for that enum. If this is not the case + /// _error is set to true and an error-message is added to the message-queue. + /// + /// @param value The enum value + /// @param schema The enum schema definition + /// + void checkEnum(const QJsonValue & value, const QJsonValue & schema); + +private: + /// The schema of the entire json-configuration + QJsonObject _qSchema; + + /// The current location into a json-configuration structure being checked + std::list _currentPath; + /// The result messages collected during the schema verification + std::list _messages; + /// Flag indicating an error occured during validation + bool _error; +}; diff --git a/libsrc/utils/CMakeLists.txt b/libsrc/utils/CMakeLists.txt index 78127e74..203dddc0 100644 --- a/libsrc/utils/CMakeLists.txt +++ b/libsrc/utils/CMakeLists.txt @@ -45,6 +45,10 @@ add_library(hyperion-utils ${CURRENT_HEADER_DIR}/jsonschema/JsonFactory.h ${CURRENT_HEADER_DIR}/jsonschema/JsonSchemaChecker.h ${CURRENT_SOURCE_DIR}/jsonschema/JsonSchemaChecker.cpp + + ${CURRENT_HEADER_DIR}/jsonschema/QJsonFactory.h + ${CURRENT_HEADER_DIR}/jsonschema/QJsonSchemaChecker.h + ${CURRENT_SOURCE_DIR}/jsonschema/QJsonSchemaChecker.cpp ) qt5_use_modules(hyperion-utils Core) diff --git a/libsrc/utils/jsonschema/QJsonSchemaChecker.cpp b/libsrc/utils/jsonschema/QJsonSchemaChecker.cpp new file mode 100644 index 00000000..f5ac74c7 --- /dev/null +++ b/libsrc/utils/jsonschema/QJsonSchemaChecker.cpp @@ -0,0 +1,375 @@ +// stdlib includes +#include +#include +#include +#include + +// Utils-Jsonschema includes +#include + +QJsonSchemaChecker::QJsonSchemaChecker() +{ + // empty +} + +QJsonSchemaChecker::~QJsonSchemaChecker() +{ + // empty +} + +bool QJsonSchemaChecker::setSchema(const QJsonObject & schema) +{ + _qSchema = schema; + + // TODO: check the schema + + return true; +} + +bool QJsonSchemaChecker::validate(const QJsonObject & value) +{ + // initialize state + _error = false; + _messages.clear(); + _currentPath.clear(); + _currentPath.push_back("[root]"); + + // validate + validate(value, _qSchema); + + return !_error; +} + +void QJsonSchemaChecker::validate(const QJsonValue & value, const QJsonObject &schema) +{ + // check the current json value + for (QJsonObject::const_iterator i = schema.begin(); i != schema.end(); ++i) + { + QString attribute = i.key(); + const QJsonValue & attributeValue = *i; + + if (attribute == "type") + checkType(value, attributeValue); + else if (attribute == "properties") + { + if (value.isObject()) + checkProperties(value.toObject(), attributeValue.toObject()); + else + { + _error = true; + setMessage("properties attribute is only valid for objects"); + continue; + } + } + else if (attribute == "additionalProperties") + { + if (value.isObject()) + { + // ignore the properties which are handled by the properties attribute (if present) + QStringList ignoredProperties; + if (schema.contains("properties")) { + const QJsonObject & props = schema["properties"].toObject(); + ignoredProperties = props.keys(); + } + + checkAdditionalProperties(value.toObject(), attributeValue, ignoredProperties); + } + else + { + _error = true; + setMessage("additional properties attribute is only valid for objects"); + continue; + } + } + else if (attribute == "minimum") + checkMinimum(value, attributeValue); + else if (attribute == "maximum") + checkMaximum(value, attributeValue); + else if (attribute == "items") + { + if (value.isArray()) + checkItems(value, attributeValue.toObject()); + else + { + _error = true; + setMessage("items only valid for arrays"); + continue; + } + } + else if (attribute == "minItems") + checkMinItems(value, attributeValue); + else if (attribute == "maxItems") + checkMaxItems(value, attributeValue); + else if (attribute == "uniqueItems") + checkUniqueItems(value, attributeValue); + else if (attribute == "enum") + checkEnum(value, attributeValue); + else if (attribute == "required") + ; // nothing to do. value is present so always oke + else if (attribute == "id") + ; // references have already been collected + else + { + // no check function defined for this attribute + setMessage(std::string("No check function defined for attribute ") + attribute.toStdString()); + continue; + } + } +} + +void QJsonSchemaChecker::setMessage(const std::string & message) +{ + std::ostringstream oss; + std::copy(_currentPath.begin(), _currentPath.end(), std::ostream_iterator(oss, "")); + oss << ": " << message; + _messages.push_back(oss.str()); +} + +const std::list & QJsonSchemaChecker::getMessages() const +{ + return _messages; +} + +void QJsonSchemaChecker::checkType(const QJsonValue & value, const QJsonValue & schema) +{ + QString type = schema.toString(); + + bool wrongType = false; + if (type == "string") + wrongType = !value.isString(); + else if (type == "number") + wrongType = !value.isDouble(); + else if (type == "integer") + wrongType = (rint(value.toDouble()) != value.toDouble()); + else if (type == "double") + wrongType = !value.isDouble(); + else if (type == "boolean") + wrongType = !value.isBool(); + else if (type == "object") + wrongType = !value.isObject(); + else if (type == "array") + wrongType = !value.isArray(); + else if (type == "null") + wrongType = !value.isNull(); + else if (type == "enum") + wrongType = !value.isString(); + else if (type == "any") + wrongType = false; +// else +// assert(false); + + if (wrongType) + { + _error = true; + setMessage(type.toStdString() + " expected"); + } +} + +void QJsonSchemaChecker::checkProperties(const QJsonObject & value, const QJsonObject & schema) +{ + for (QJsonObject::const_iterator i = schema.begin(); i != schema.end(); ++i) + { + QString property = i.key(); + + const QJsonValue & propertyValue = i.value(); + + _currentPath.push_back(std::string(".") + property.toStdString()); + QJsonObject::const_iterator required = propertyValue.toObject().find("required"); + + if (value.contains(property)) + { + validate(value[property], propertyValue.toObject()); + } + else if (required != schema.end() && required.value().toBool()) + { + _error = true; + setMessage("missing member"); + } + _currentPath.pop_back(); + } +} + +void QJsonSchemaChecker::checkAdditionalProperties(const QJsonObject & value, const QJsonValue & schema, const QStringList & ignoredProperties) +{ + for (QJsonObject::const_iterator i = value.begin(); i != value.end(); ++i) + { + QString property = i.key(); + if (std::find(ignoredProperties.begin(), ignoredProperties.end(), property) == ignoredProperties.end()) + { + // property has no property definition. check against the definition for additional properties + _currentPath.push_back(std::string(".") + property.toStdString()); + if (schema.isBool()) + { + if (schema.toBool() == false) + { + _error = true; + setMessage("no schema definition"); + } + } + else + { + validate(value[property], schema.toObject()); + } + _currentPath.pop_back(); + } + } +} + +void QJsonSchemaChecker::checkMinimum(const QJsonValue & value, const QJsonValue & schema) +{ + if (!value.isDouble()) + { + // only for numeric + _error = true; + setMessage("minimum check only for numeric fields"); + return; + } + + if (value.toDouble() < schema.toDouble()) + { + _error = true; + std::ostringstream oss; + oss << "value is too small (minimum=" << schema.toDouble() << ")"; + setMessage(oss.str()); + } +} + +void QJsonSchemaChecker::checkMaximum(const QJsonValue & value, const QJsonValue & schema) +{ + if (!value.isDouble()) + { + // only for numeric + _error = true; + setMessage("maximum check only for numeric fields"); + return; + } + + if (value.toDouble() > schema.toDouble()) + { + _error = true; + std::ostringstream oss; + oss << "value is too large (maximum=" << schema.toDouble() << ")"; + setMessage(oss.str()); + } +} + +void QJsonSchemaChecker::checkItems(const QJsonValue & value, const QJsonObject & schema) +{ + if (!value.isArray()) + { + // only for arrays + _error = true; + setMessage("items only valid for arrays"); + return; + } + + QJsonArray jArray = value.toArray(); + for(int i = 0; i < jArray.size(); ++i) + { + // validate each item + std::ostringstream oss; + oss << "[" << i << "]"; + _currentPath.push_back(oss.str()); + validate(jArray[i].toObject(), schema); + _currentPath.pop_back(); + } +} + +void QJsonSchemaChecker::checkMinItems(const QJsonValue & value, const QJsonValue & schema) +{ + if (!value.isArray()) + { + // only for arrays + _error = true; + setMessage("minItems only valid for arrays"); + return; + } + + int minimum = schema.toInt(); + + QJsonArray jArray = value.toArray(); + if (static_cast(jArray.size()) < minimum) + { + _error = true; + std::ostringstream oss; + oss << "array is too small (minimum=" << minimum << ")"; + setMessage(oss.str()); + } +} + +void QJsonSchemaChecker::checkMaxItems(const QJsonValue & value, const QJsonValue & schema) +{ + if (!value.isArray()) + { + // only for arrays + _error = true; + setMessage("maxItems only valid for arrays"); + return; + } + + int maximum = schema.toInt(); + + QJsonArray jArray = value.toArray(); + if (static_cast(jArray.size()) > maximum) + { + _error = true; + std::ostringstream oss; + oss << "array is too large (maximum=" << maximum << ")"; + setMessage(oss.str()); + } +} + +void QJsonSchemaChecker::checkUniqueItems(const QJsonValue & value, const QJsonValue & schema) +{ + if (!value.isArray()) + { + // only for arrays + _error = true; + setMessage("uniqueItems only valid for arrays"); + return; + } + + if (schema.toBool() == true) + { + // make sure no two items are identical + + QJsonArray jArray = value.toArray(); + for(int i = 0; i < jArray.size(); ++i) + { + for (int j = i+1; j < jArray.size(); ++j) + { + if (jArray[i] == jArray[j]) + { + // found a value twice + _error = true; + setMessage("array must have unique values"); + } + } + } + } +} + +void QJsonSchemaChecker::checkEnum(const QJsonValue & value, const QJsonValue & schema) +{ + if (schema.isArray()) + { + QJsonArray jArray = schema.toArray(); + for(int i = 0; i < jArray.size(); ++i) + { + if (jArray[i] == value) + { + // found enum value. done. + return; + } + } + } + + // nothing found + _error = true; + std::ostringstream oss; + oss << "Unknown enum value (allowed values are: " << schema.toString().toStdString(); + QJsonDocument doc(schema.toArray()); + QString strJson(doc.toJson(QJsonDocument::Compact)); + oss << strJson.toStdString() << ")"; + setMessage(oss.str()); +} \ No newline at end of file diff --git a/test/TestConfigFile.cpp b/test/TestConfigFile.cpp index be1e8880..00c1d23b 100644 --- a/test/TestConfigFile.cpp +++ b/test/TestConfigFile.cpp @@ -6,36 +6,76 @@ #include // JsonSchema includes -#include +#include // hyperion includes #include -Json::Value loadConfig(const std::string & configFile) +bool loadConfig(const QString & configFile) { // make sure the resources are loaded (they may be left out after static linking) Q_INIT_RESOURCE(resource); - - // read the json schema from the resource - QResource schemaData(":/hyperion-schema"); - if (!schemaData.isValid()) \ + QJsonParseError error; + + //////////////////////////////////////////////////////////// + // read and set the json schema from the resource + //////////////////////////////////////////////////////////// + + QFile schemaData(":/hyperion-schema"); + + if (!schemaData.open(QIODevice::ReadOnly)) { - throw std::runtime_error("Schema not found"); + std::stringstream error; + error << "Schema not found: " << schemaData.errorString().toStdString(); + throw std::runtime_error(error.str()); } - Json::Reader jsonReader; - Json::Value schemaJson; - if (!jsonReader.parse(reinterpret_cast(schemaData.data()), reinterpret_cast(schemaData.data()) + schemaData.size(), schemaJson, false)) + QByteArray schema = schemaData.readAll(); + QJsonDocument schemaJson = QJsonDocument::fromJson(schema, &error); + + if (error.error != QJsonParseError::NoError) { - throw std::runtime_error("Schema error: " + jsonReader.getFormattedErrorMessages()) ; + // report to the user the failure and their locations in the document. + int errorLine(0), errorColumn(0); + + for( int i=0, count=qMin( error.offset,schema.size()); i::const_iterator i = schemaChecker.getMessages().begin(); i != schemaChecker.getMessages().end(); ++i) + { + std::cout << *i << std::endl; + } + + std::cout << "FAILED" << std::endl; + exit(1); + return false; } - JsonSchemaChecker schemaChecker; - schemaChecker.setSchema(schemaJson); - const Json::Value jsonConfig = JsonFactory::readJson(configFile); - schemaChecker.validate(jsonConfig); - - return jsonConfig; + return true; } int main(int argc, char** argv) @@ -47,19 +87,20 @@ int main(int argc, char** argv) return 0; } - const std::string configFile(argv[1]); - std::cout << "Configuration file selected: " << configFile.c_str() << std::endl; - std::cout << "Attemp to load...\t"; + const QString configFile(argv[1]); + std::cout << "Configuration file selected: " << configFile.toStdString() << std::endl; + std::cout << "Attemp to load..." << std::endl; try { - Json::Value value = loadConfig(configFile); - (void)value; - std::cout << "PASSED" << std::endl; + if (loadConfig(configFile)) + std::cout << "PASSED" << std::endl; + exit(0); } catch (std::runtime_error exception) { std::cout << "FAILED" << std::endl; std::cout << exception.what() << std::endl; + exit(1); } return 0;