diff --git a/Common/columnencoder.cpp b/Common/columnencoder.cpp index 4f90638d05..1015338f24 100644 --- a/Common/columnencoder.cpp +++ b/Common/columnencoder.cpp @@ -827,6 +827,7 @@ void ColumnEncoder::_addTypeToColumnNamesInOptionsRecursively(Json::Value & opti ColumnEncoder::colsPlusTypes ColumnEncoder::encodeColumnNamesinOptions(Json::Value & options, bool preloadingData) { + columnEncoder(); colsPlusTypes getTheseCols; _addTypeToColumnNamesInOptionsRecursively(options, preloadingData, getTheseCols); diff --git a/Common/columnencoder.h b/Common/columnencoder.h index bfd798dad2..a993985a4d 100644 --- a/Common/columnencoder.h +++ b/Common/columnencoder.h @@ -67,6 +67,7 @@ private: ColumnEncoder() { invalidateAll(); } bool shouldEncode(const std::string & in); bool shouldDecode(const std::string & in); void setCurrentNames(const colTypeMap & names); + const colTypeMap& currentNames() const { return _dataSetTypes; } void updateColumnTypesOnly(const colTypeMap & names); void setCurrentNames(const std::vector & names, bool generateTypesEncoding=true); ///< Do not use! Deprecated void setCurrentColumnTypePerName(const colTypeMap & theMap); ///< Do not use! Deprecated @@ -87,7 +88,7 @@ private: ColumnEncoder() { invalidateAll(); } static std::string encodeAll(const std::string & text) { return replaceAll(text, encodingMap(), originalNames()); } ///Replace all occurences of encoded columnNames in a string by their decoded versions, regardless of word boundaries or parentheses. - static std::string decodeAll(const std::string & text) { return replaceAll(text, decodingMap(), encodedNames()); } + static std::string decodeAll(const std::string & text) { columnEncoder(); return replaceAll(text, decodingMap(), encodedNames()); } ///Replace all occurences of columnNames in a string by their encoded versions in all json-names and string-values, regardless of word boundaries or parentheses. static void encodeJson(Json::Value & json, bool replaceNames = false, bool replaceStrict = false); diff --git a/Common/columnencodercontext.cpp b/Common/columnencodercontext.cpp new file mode 100644 index 0000000000..d31cbad17d --- /dev/null +++ b/Common/columnencodercontext.cpp @@ -0,0 +1,144 @@ +// +// Copyright (C) 2013-2025 University of Amsterdam +// +// 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. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +#include "columnencodercontext.h" + +#include + +static Json::Value columnTypesToJson(const ColumnEncoder::colTypeMap & columnTypes) +{ + Json::Value columns(Json::arrayValue); + for(const auto & nameType : columnTypes) + { + Json::Value column(Json::objectValue); + column["name"] = nameType.first; + column["type"] = columnTypeToString(nameType.second); + columns.append(column); + } + + return columns; +} + +static ColumnEncoder::colTypeMap columnTypesFromJson(const Json::Value & columns, const char * fieldName) +{ + ColumnEncoder::colTypeMap columnTypes; + + if(columns.isNull()) + return columnTypes; + if(!columns.isArray()) + throw std::runtime_error(std::string("Column encoder context field '") + fieldName + "' must be an array."); + + for(const Json::Value & column : columns) + { + if(!column.isObject() || !column["name"].isString() || !column["type"].isString()) + throw std::runtime_error(std::string("Column encoder context field '") + fieldName + "' must contain objects with string 'name' and 'type' fields."); + + columnTypes[column["name"].asString()] = columnTypeFromString(column["type"].asString()); + } + + return columnTypes; +} + +static Json::Value parsePayloadJson(const char * payloadJson) +{ + if(!payloadJson) + throw std::runtime_error("Cannot decode column text from a null JSON payload."); + + Json::Value payload; + Json::Reader reader; + if(!reader.parse(payloadJson, payload)) + throw std::runtime_error("Could not parse column text JSON payload."); + + return payload; +} + +ColumnEncoderContext::ColumnEncoderContext(const ColumnEncoder::colTypeMap & columns, const ColumnEncoder::colTypeMap & extra) + : _columns(columns), _extra(extra), _supplied(true) +{ +} + +ColumnEncoderContext ColumnEncoderContext::fromJson(const Json::Value & context) +{ + if(!context.isObject()) + throw std::runtime_error("Column encoder context must be a JSON object."); + + if(!context.isMember("version") || !context["version"].isInt()) + throw std::runtime_error("Column encoder context must contain integer version 1."); + if(context["version"].asInt() != Version) + throw std::runtime_error("Unsupported column encoder context version."); + + return ColumnEncoderContext( + columnTypesFromJson(context["columns"], "columns"), + columnTypesFromJson(context["extra"], "extra") + ); +} + +ColumnEncoderContext ColumnEncoderContext::fromJsonString(const char * contextJson) +{ + if(!contextJson || std::string(contextJson).empty()) + return ColumnEncoderContext(); + + Json::Value context; + Json::Reader reader; + if(!reader.parse(contextJson, context)) + throw std::runtime_error("Could not parse column encoder context JSON."); + + return fromJson(context); +} + +Json::Value ColumnEncoderContext::toJson() const +{ + Json::Value context(Json::objectValue); + context["version"] = Version; + context["columns"] = columnTypesToJson(_columns); + context["extra"] = columnTypesToJson(_extra); + + return context; +} + +ScopedColumnEncoderContext::ScopedColumnEncoderContext(const ColumnEncoderContext & context, ColumnEncoder & extraEncoder) + : _supplied(context.supplied()), _extraEncoder(extraEncoder) +{ + if(!_supplied) + return; + + _previousColumns = ColumnEncoder::columnEncoder()->currentNames(); + _previousExtra = _extraEncoder.currentNames(); + + ColumnEncoder::columnEncoder()->setCurrentNames(context.columns()); + _extraEncoder.setCurrentNames(context.extra()); +} + +ScopedColumnEncoderContext::~ScopedColumnEncoderContext() +{ + if(!_supplied) + return; + + ColumnEncoder::columnEncoder()->setCurrentNames(_previousColumns); + _extraEncoder.setCurrentNames(_previousExtra); +} + +Json::Value decodeColumnJson(const char * payloadJson, const char * encoderContextJson, ColumnEncoder & extraEncoder, bool replaceNames) +{ + Json::Value payload = parsePayloadJson(payloadJson); + ColumnEncoderContext context = ColumnEncoderContext::fromJsonString(encoderContextJson); + ScopedColumnEncoderContext scopedContext(context, extraEncoder); + + ColumnEncoder::decodeJson(payload, replaceNames); + + return payload; +} diff --git a/Common/columnencodercontext.h b/Common/columnencodercontext.h new file mode 100644 index 0000000000..181c019edd --- /dev/null +++ b/Common/columnencodercontext.h @@ -0,0 +1,61 @@ +// +// Copyright (C) 2013-2025 University of Amsterdam +// +// 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. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +#ifndef COLUMNENCODERCONTEXT_H +#define COLUMNENCODERCONTEXT_H + +#include "columnencoder.h" + +class ColumnEncoderContext +{ +public: + static constexpr int Version = 1; + + ColumnEncoderContext() = default; + ColumnEncoderContext(const ColumnEncoder::colTypeMap & columns, const ColumnEncoder::colTypeMap & extra); + + static ColumnEncoderContext fromJson(const Json::Value & context); + static ColumnEncoderContext fromJsonString(const char * contextJson); + + Json::Value toJson() const; + + const ColumnEncoder::colTypeMap& columns() const { return _columns; } + const ColumnEncoder::colTypeMap& extra() const { return _extra; } + bool supplied() const { return _supplied; } + +private: + ColumnEncoder::colTypeMap _columns; + ColumnEncoder::colTypeMap _extra; + bool _supplied = false; +}; + +class ScopedColumnEncoderContext +{ +public: + ScopedColumnEncoderContext(const ColumnEncoderContext & context, ColumnEncoder & extraEncoder); + ~ScopedColumnEncoderContext(); + +private: + bool _supplied = false; + ColumnEncoder & _extraEncoder; + ColumnEncoder::colTypeMap _previousColumns; + ColumnEncoder::colTypeMap _previousExtra; +}; + +Json::Value decodeColumnJson(const char * payloadJson, const char * encoderContextJson, ColumnEncoder & extraEncoder, bool replaceNames = true); + +#endif // COLUMNENCODERCONTEXT_H diff --git a/CommonData/databridge.cpp b/CommonData/databridge.cpp index dfb51a11bb..9bc78931ed 100644 --- a/CommonData/databridge.cpp +++ b/CommonData/databridge.cpp @@ -23,6 +23,7 @@ #include "timers.h" DataBridge::DataBridge(unsigned long sessionID, bool useMemory) + : _extraEncodings(new ColumnEncoder(ExtraOptionsPrefix)) { JASPTIMER_START(TempFiles Attach); TempFiles::attach(sessionID); diff --git a/CommonData/databridge.h b/CommonData/databridge.h index 0f0c2b6634..e1d09f1507 100644 --- a/CommonData/databridge.h +++ b/CommonData/databridge.h @@ -21,6 +21,10 @@ #include "dataset.h" +#include + +class ColumnEncoder; + class DataBridge { public: @@ -44,6 +48,8 @@ class DataBridge void provideSpecificFileName( const std::string & specificName, std::string & root, std::string & relativePath); int dataSetRowCount() { return static_cast(provideAndUpdateDataSet()->rowCount()); } void updateOptionsAccordingToMeta(Json::Value & options); + ColumnEncoder * extraEncodings() { return _extraEncodings.get(); } + const ColumnEncoder * extraEncodings() const { return _extraEncodings.get(); } protected: bool isColumnNameOk(const std::string & columnName); @@ -54,6 +60,10 @@ class DataBridge DatabaseInterface * _db = nullptr; int _analysisId = -1; std::function _datasetProvidedCallback; + +private: + static constexpr const char * ExtraOptionsPrefix = "JaspExtraOptions_"; + std::unique_ptr _extraEncodings; }; #endif // DATABRIDGE_H diff --git a/CommonData/rbridge.cpp b/CommonData/rbridge.cpp index d3df6ed30e..8d36db04d5 100644 --- a/CommonData/rbridge.cpp +++ b/CommonData/rbridge.cpp @@ -65,12 +65,12 @@ void rbridge_setDataBridge(DataBridge * dataBridge) { data_bridge = dataBridge; rbridge_dataSet = nullptr; + extraEncodings = dataBridge ? dataBridge->extraEncodings() : nullptr; } void rbridge_clearDataBridge() { - data_bridge = nullptr; - rbridge_dataSet = nullptr; + rbridge_setDataBridge(nullptr); } const std::string jaspBaseDistributionSamplersR = @@ -102,14 +102,12 @@ const std::string jaspBaseTransformPowerR = #include "jaspBase_transformPower.h" ; -void rbridge_init(DataBridge * dataBridge, sendFuncDef sendToDesktopFunction, pollMessagesFuncDef pollMessagesFunction, ColumnEncoder * extraEncoder, const char * resultFont, bool insideJasp) +void rbridge_init(DataBridge * dataBridge, sendFuncDef sendToDesktopFunction, pollMessagesFuncDef pollMessagesFunction, const char * resultFont, bool insideJasp) { JASPTIMER_SCOPE(rbridge_init); + Log::log() << "Setting DataBridge and extraEncodings." << std::endl; rbridge_setDataBridge(dataBridge); - - Log::log() << "Setting extraEncodings." << std::endl; - extraEncodings = extraEncoder; Log::log() << "Collecting RBridgeCallBacks." << std::endl; RBridgeCallBacks callbacks = { @@ -211,12 +209,12 @@ extern "C" int STDCALL rbridge_decodeColumnType(const char * in) extern "C" bool STDCALL rbridge_shouldEncodeColumnName(const char * in) { - return ColumnEncoder::columnEncoder()->shouldEncode(in); + return (extraEncodings && extraEncodings->shouldEncode(in)) || ColumnEncoder::columnEncoder()->shouldEncode(in); } extern "C" bool STDCALL rbridge_shouldDecodeColumnName(const char * in) { - return ColumnEncoder::columnEncoder()->shouldDecode(in); + return (extraEncodings && extraEncodings->shouldDecode(in)) || ColumnEncoder::columnEncoder()->shouldDecode(in); } extern "C" const char * STDCALL rbridge_encodeAllColumnNames(const char * in) diff --git a/CommonData/rbridge.h b/CommonData/rbridge.h index 997e23fbd6..a29f9b8ce3 100644 --- a/CommonData/rbridge.h +++ b/CommonData/rbridge.h @@ -82,7 +82,7 @@ extern "C" { void rbridge_setDataBridge(DataBridge * dataBridge); void rbridge_clearDataBridge(); - void rbridge_init(DataBridge * dataBridge, sendFuncDef sendToDesktopFunction, pollMessagesFuncDef pollMessagesFunction, ColumnEncoder * encoder, const char * resultFont, bool insideJasp = true); + void rbridge_init(DataBridge * dataBridge, sendFuncDef sendToDesktopFunction, pollMessagesFuncDef pollMessagesFunction, const char * resultFont, bool insideJasp = true); void rbridge_memoryCleaning(); diff --git a/Engine/engine.cpp b/Engine/engine.cpp index ca54c7d57f..4ab1312ec3 100644 --- a/Engine/engine.cpp +++ b/Engine/engine.cpp @@ -75,8 +75,6 @@ Engine::Engine(int slaveNo, unsigned long parentPID) JASPTIMER_SCOPE(Engine Constructor); assert(_EngineInstance == NULL); _EngineInstance = this; - - _extraEncodings = new ColumnEncoder("JaspExtraOptions_"); } void Engine::initialize() @@ -88,7 +86,7 @@ void Engine::initialize() std::string memoryName = "JASP-IPC-" + std::to_string(_parentPID); _channel = new IPCChannel(memoryName, _engineNum, true); - rbridge_init(this, SendFunctionForJaspresults, PollMessagesFunctionForJaspResults, _extraEncodings, _resultFont.c_str()); + rbridge_init(this, SendFunctionForJaspresults, PollMessagesFunctionForJaspResults, _resultFont.c_str()); Log::log() << "rbridge_init completed" << std::endl; @@ -679,7 +677,7 @@ void Engine::receiveAnalysisMessage(const Json::Value & jsonRequest) Log::log(false) << _analysisTitle << " with ID " << _analysisId << std::endl; - _extraEncodings->setCurrentNamesFromOptionsMeta(optionsEnc); + extraEncodings()->setCurrentNamesFromOptionsMeta(optionsEnc); _analysisOptions = optionsEnc; //store unencoded } diff --git a/Engine/engine.h b/Engine/engine.h index f29bfc099b..6963263776 100644 --- a/Engine/engine.h +++ b/Engine/engine.h @@ -97,7 +97,6 @@ class Engine : public DataBridge const int _engineNum; const unsigned long _parentPID; IPCChannel * _channel = nullptr; - ColumnEncoder * _extraEncodings = nullptr; engineState _engineState = engineState::initializing, _lastRequest = engineState::initializing; Status _analysisStatus = Status::empty; diff --git a/SyntaxInterface/CMakeLists.txt b/SyntaxInterface/CMakeLists.txt index 756a95fd74..ff46fc6c43 100644 --- a/SyntaxInterface/CMakeLists.txt +++ b/SyntaxInterface/CMakeLists.txt @@ -48,6 +48,10 @@ target_compile_definitions( JASP_R_INTERFACE_LIBRARY ) +if(MSVC) + target_compile_options(SyntaxInterface PRIVATE /EHsc) +endif() + if(USE_QT_STATIC_LIBS) target_compile_definitions( SyntaxInterface diff --git a/SyntaxInterface/syntaxbridge.cpp b/SyntaxInterface/syntaxbridge.cpp index 82aa02a711..ed78a564ae 100644 --- a/SyntaxInterface/syntaxbridge.cpp +++ b/SyntaxInterface/syntaxbridge.cpp @@ -43,7 +43,11 @@ #include "modules/dynamicmodule.h" #include "archivereader.h" #include "databaseinterface.h" +#include "columnencoder.h" +#include "columnencodercontext.h" +#include "boost/iostreams/stream.hpp" +#include #include #include @@ -63,7 +67,6 @@ static bool gl_jaspBaseInitialized = false; static QGuiApplication * gl_application = nullptr; static QQmlEngine * gl_qmlEngine = nullptr; static DataBridge * gl_dataBridge = nullptr; -static ColumnEncoder * gl_extraEncodings = nullptr; static QMap > gl_qmlFormMap; static int gl_applicationArgc = 0; static std::vector gl_applicationArgvStorage; @@ -85,6 +88,24 @@ static std::string gl_param_resultFont = "freesans,sans-serif"; #endif +namespace +{ + boost::iostreams::stream gl_nullLogStream((boost::iostreams::null_sink())); + bool gl_loggingInitialized = false; +} + +static void configureBridgeLogging(bool verbose) +{ + if(!gl_loggingInitialized) + { + Log::init(&gl_nullLogStream); + gl_loggingInitialized = true; + } + + Log::setDefaultDestination(verbose ? logType::cout : logType::null); + Log::setWhere(verbose ? logType::cout : logType::null); +} + static bool readJaspJsonEntry(Json::Value & root, const char * filePath, const char * entry, std::string * error = nullptr) { try @@ -148,10 +169,39 @@ static const char* statusError(Json::Value status, const std::string & error) { status["ok"] = false; status["error"] = error; - Log::log() << error << std::endl; return statusResult(status); } +static ColumnEncoder * extraColumnEncoder() +{ + return gl_dataBridge ? gl_dataBridge->extraEncodings() : nullptr; +} + +static ColumnEncoder & requireExtraColumnEncoder() +{ + ColumnEncoder * encoder = extraColumnEncoder(); + if(!encoder) + throw std::runtime_error("Cannot access extra option encodings without an initialized DataBridge."); + + return *encoder; +} + +static ColumnEncoder::colTypeMap currentDatasetColumnTypes() +{ + DataSet * dataSet = gl_dataBridge ? gl_dataBridge->provideAndUpdateDataSet() : nullptr; + return dataSet ? dataSet->getColumnTypesMap() : ColumnEncoder::colTypeMap(); +} + +static Json::Value columnEncoderContextJson() +{ + ColumnEncoder * extraEncoder = extraColumnEncoder(); + + return ColumnEncoderContext( + currentDatasetColumnTypes(), + extraEncoder ? extraEncoder->currentNames() : ColumnEncoder::colTypeMap() + ).toJson(); +} + static Json::Value analysisOptionsStatus(const char * filePath, int analysisNr) { Json::Value status = statusBase("syntaxBridgeAnalysisOptionsFromJaspFile"); @@ -202,8 +252,8 @@ static void clearRequestedDataState() ColumnEncoder::colTypeMap noColumns; ColumnEncoder::columnEncoder()->setCurrentNames(noColumns); - if (gl_extraEncodings) - gl_extraEncodings->setCurrentNames(noColumns); + if (ColumnEncoder * encoder = extraColumnEncoder()) + encoder->setCurrentNames(noColumns); } static void clearDataBridgeState() @@ -315,12 +365,6 @@ void STDCALL syntaxBridgeShutdown() syntaxBridgeClearQmlState(); clearDataBridgeState(); - if (gl_extraEncodings) - { - delete gl_extraEncodings; - gl_extraEncodings = nullptr; - } - if (gl_initialized) { DataSetProvider * provider = DataSetProvider::getProvider(gl_initializedDbInMemory, false, gl_application); @@ -459,11 +503,22 @@ const char* STDCALL syntaxBridgeLoadDataSetFromJaspFileStatus(const char * fileP const char* STDCALL syntaxBridgeLoadQmlAndParseOptions(const char* moduleName, const char* analysisName, const char* qmlFile, const char* options, const char* version, bool preloadData) { - if (!init()) - { - Log::log() << "Error during initialization" << std::endl; + Json::Value status; + Json::Reader reader; + if (!reader.parse(syntaxBridgeLoadQmlAndParseOptionsStatus(moduleName, analysisName, qmlFile, options, version, preloadData), status)) return ""; - } + if (!status["ok"].asBool()) + return ""; + + static std::string result; + result = status["options"].toStyledString(); + return result.c_str(); +} + +const char* STDCALL syntaxBridgeLoadQmlAndParseOptionsStatus(const char* moduleName, const char* analysisName, const char* qmlFile, const char* options, const char* version, bool preloadData) +{ + if (!init()) + return statusError(statusBase("syntaxBridgeLoadQmlAndParseOptions"), "Error during initialization."); std::string qmlFileStr = qmlFile, versionStr = version, @@ -475,8 +530,7 @@ const char* STDCALL syntaxBridgeLoadQmlAndParseOptions(const char* moduleName, c if (!form) { - Log::log() << "Cannot create QML Form " << qmlFileStr << std::endl; - return ""; + return statusError(statusBase("syntaxBridgeLoadQmlAndParseOptions"), "Cannot create QML Form " + qmlFileStr); } Json::Value parsedOptions; @@ -484,20 +538,19 @@ const char* STDCALL syntaxBridgeLoadQmlAndParseOptions(const char* moduleName, c if (!form->parseOptions(options, parsedOptions, errorMsg)) { - Log::log() << "Error when parsing options: " << errorMsg << std::endl; - return ""; + return statusError(statusBase("syntaxBridgeLoadQmlAndParseOptions"), "Error when parsing options: " + errorMsg); } - gl_extraEncodings->setCurrentNamesFromOptionsMeta(parsedOptions); + gl_dataBridge->extraEncodings()->setCurrentNamesFromOptionsMeta(parsedOptions); gl_dataBridge->updateOptionsAccordingToMeta(parsedOptions); ColumnEncoder::colsPlusTypes analysisColsTypes = ColumnEncoder::encodeColumnNamesinOptions(parsedOptions, preloadData); rbridge_setWantedCols(analysisColsTypes); - static std::string result; - result = parsedOptions.toStyledString(); - - return result.c_str(); + Json::Value status = statusBase("syntaxBridgeLoadQmlAndParseOptions"); + status["ok"] = true; + status["options"] = parsedOptions; + return statusResult(status); } const char* STDCALL syntaxBridgeAnalysisOptionsFromJaspFile(const char * filePath, int analysisNr) @@ -507,11 +560,7 @@ const char* STDCALL syntaxBridgeAnalysisOptionsFromJaspFile(const char * filePat Json::Value status = analysisOptionsStatus(filePath, analysisNr); if (!status["ok"].asBool()) - { - if (status.isMember("error")) - Log::log() << status["error"].asString() << std::endl; return result.c_str(); - } result = status["options"].toStyledString(); return result.c_str(); @@ -520,8 +569,6 @@ const char* STDCALL syntaxBridgeAnalysisOptionsFromJaspFile(const char * filePat const char* STDCALL syntaxBridgeAnalysisOptionsFromJaspFileStatus(const char * filePath, int analysisNr) { Json::Value status = analysisOptionsStatus(filePath, analysisNr); - if (!status["ok"].asBool() && status.isMember("error")) - Log::log() << status["error"].asString() << std::endl; return statusResult(status); } @@ -655,6 +702,40 @@ const char* STDCALL syntaxBridgeGetVariableNames() return result.c_str(); } +void STDCALL syntaxBridgeSetVerbose(bool verbose) +{ + gl_verbose = verbose; + if (gl_loggingInitialized) + configureBridgeLogging(verbose); +} + +const char* STDCALL syntaxBridgeColumnEncoderContext() +{ + static std::string result; + + result = columnEncoderContextJson().toStyledString(); + return result.c_str(); +} + +const char* STDCALL syntaxBridgeDecodeColumnText(const char* valuesJson, const char* encoderContextJson) +{ + static std::string result; + + try + { + result = decodeColumnJson(valuesJson, encoderContextJson, requireExtraColumnEncoder()).toStyledString(); + return result.c_str(); + } + catch(const std::exception & exception) + { + return statusError(statusBase("syntaxBridgeDecodeColumnText"), exception.what()); + } + catch(...) + { + return statusError(statusBase("syntaxBridgeDecodeColumnText"), "Unknown error while decoding column text."); + } +} + } // extern "C" @@ -681,6 +762,8 @@ void sendMessage(const char * msg) bool init(bool dbInMemory) { + configureBridgeLogging(gl_verbose); + if (gl_initialized) return true; gl_initialized = true; gl_initializedDbInMemory = dbInMemory; @@ -721,9 +804,8 @@ bool init(bool dbInMemory) QmlUtils::registerQmlModuleTypes(); createDataBridge(dbInMemory); - gl_extraEncodings = new ColumnEncoder("JaspExtraOptions_"); - rbridge_init(gl_dataBridge, sendMessage, [](){ return false; }, gl_extraEncodings, gl_param_resultFont.c_str(), false); + rbridge_init(gl_dataBridge, sendMessage, [](){ return false; }, gl_param_resultFont.c_str(), false); gl_rBridgeInitialized = true; return true; @@ -734,7 +816,7 @@ void ensureRBridgeInitialized() if (gl_rBridgeInitialized) return; - rbridge_init(gl_dataBridge, sendMessage, [](){ return false; }, gl_extraEncodings, gl_param_resultFont.c_str(), false); + rbridge_init(gl_dataBridge, sendMessage, [](){ return false; }, gl_param_resultFont.c_str(), false); gl_rBridgeInitialized = true; } diff --git a/SyntaxInterface/syntaxbridge_interface.h b/SyntaxInterface/syntaxbridge_interface.h index e228746288..8c531a21fd 100644 --- a/SyntaxInterface/syntaxbridge_interface.h +++ b/SyntaxInterface/syntaxbridge_interface.h @@ -63,12 +63,16 @@ SYNTAX_INTERFACE void STDCALL syntaxBridgeLoadDataSet(const SyntaxBridgeDataS SYNTAX_INTERFACE void STDCALL syntaxBridgeLoadDataSetFromJaspFile(const char * filePath, bool dbInMemory); SYNTAX_INTERFACE const char* STDCALL syntaxBridgeLoadDataSetFromJaspFileStatus(const char * filePath, bool dbInMemory); SYNTAX_INTERFACE const char* STDCALL syntaxBridgeLoadQmlAndParseOptions(const char * moduleName, const char* analysisName, const char* qmlFile, const char* options, const char* version, bool preloadData); +SYNTAX_INTERFACE const char* STDCALL syntaxBridgeLoadQmlAndParseOptionsStatus(const char * moduleName, const char* analysisName, const char* qmlFile, const char* options, const char* version, bool preloadData); SYNTAX_INTERFACE const char* STDCALL syntaxBridgeAnalysisOptionsFromJaspFile(const char * filePath, int analysisNr); SYNTAX_INTERFACE const char* STDCALL syntaxBridgeAnalysisOptionsFromJaspFileStatus(const char * filePath, int analysisNr); SYNTAX_INTERFACE const char* STDCALL syntaxBridgeGenerateModuleWrappers(const char* name); SYNTAX_INTERFACE const char* STDCALL syntaxBridgeGenerateAnalysisWrapper(const char* modulePath, const char* analysisName); SYNTAX_INTERFACE const char* STDCALL syntaxBridgeParseDescription(const char* modulePath); SYNTAX_INTERFACE const char* STDCALL syntaxBridgeGetVariableNames(); +SYNTAX_INTERFACE void STDCALL syntaxBridgeSetVerbose(bool verbose); +SYNTAX_INTERFACE const char* STDCALL syntaxBridgeColumnEncoderContext(); +SYNTAX_INTERFACE const char* STDCALL syntaxBridgeDecodeColumnText(const char* valuesJson, const char* encoderContextJson); } // extern "C" diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt index 65e88ab6bd..729816b800 100644 --- a/Tests/CMakeLists.txt +++ b/Tests/CMakeLists.txt @@ -46,12 +46,22 @@ qt_add_executable( $<$:${CMAKE_SOURCE_DIR}/Desktop/JASP.exe.manifest> ) +qt_add_executable( + JASPTestColumnEncoderContext + ${SYSTEM_TYPE} + testcolumnencodercontext.h + testcolumnencodercontext.cpp + $<$:${_R_Framework}> + $<$:${CMAKE_SOURCE_DIR}/Desktop/JASP.exe.manifest> +) + add_dependencies(JASPTest JASPDesktopLib) add_dependencies(JASPTestDebugData JASPDesktopLib) add_dependencies(JASPTestEngine JASPDesktopLib) add_dependencies(JASPQuickTest JASPDesktopLib) add_dependencies(JASPTestCsvPrev JASPDesktopLib) +add_dependencies(JASPTestColumnEncoderContext Common) target_include_directories( JASPTest @@ -86,6 +96,12 @@ target_include_directories( JASPDesktopLib ) +target_include_directories( + JASPTestColumnEncoderContext + PUBLIC + ${PROJECT_SOURCE_DIR}/Common +) + target_link_libraries( JASPTest PUBLIC @@ -135,6 +151,13 @@ target_link_libraries( Qt::Test ) +target_link_libraries( + JASPTestColumnEncoderContext + PUBLIC + Common + Qt::Test +) + target_compile_definitions( JASPTest PUBLIC TESTLIBRARY_DIR=${CMAKE_SOURCE_DIR}/Tests/TestLibrary @@ -171,6 +194,7 @@ add_test(NAME testCsvPreview COMMAND JASPTestCsvPrev) add_test(NAME testDebugData COMMAND JASPTestDebugData) add_test(NAME testEngine COMMAND JASPTestEngine) add_test(NAME testQuick COMMAND JASPQuickTest) +add_test(NAME testColumnEncoderContext COMMAND JASPTestColumnEncoderContext) diff --git a/Tests/testcolumnencodercontext.cpp b/Tests/testcolumnencodercontext.cpp new file mode 100644 index 0000000000..a2443637ea --- /dev/null +++ b/Tests/testcolumnencodercontext.cpp @@ -0,0 +1,222 @@ +// +// Copyright (C) 2026 University of Amsterdam +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public +// License along with this program. If not, see +// . +// + +#include "testcolumnencodercontext.h" + +#include "columnencodercontext.h" + +#include + +#include +#include +#include + +namespace +{ +constexpr const char * ExtraOptionsPrefix = "JaspExtraOptions_"; + +ColumnEncoder::colTypeMap emptyNames() +{ + return ColumnEncoder::colTypeMap(); +} + +ColumnEncoder::colTypeMap names(std::initializer_list> values) +{ + ColumnEncoder::colTypeMap result; + for(const auto & value : values) + result[value.first] = value.second; + return result; +} + +ColumnEncoderContext context(ColumnEncoder::colTypeMap columns, ColumnEncoder::colTypeMap extra) +{ + return ColumnEncoderContext(columns, extra); +} + +std::string contextJson(const ColumnEncoderContext & encoderContext) +{ + return encoderContext.toJson().toStyledString(); +} + +void activateContext(const ColumnEncoderContext & encoderContext, ColumnEncoder & extraEncoder) +{ + ColumnEncoder::columnEncoder()->setCurrentNames(encoderContext.columns()); + extraEncoder.setCurrentNames(encoderContext.extra()); +} + +std::string encodeColumn(const ColumnEncoderContext & encoderContext, ColumnEncoder & extraEncoder, const std::string & column) +{ + ScopedColumnEncoderContext scopedContext(encoderContext, extraEncoder); + return ColumnEncoder::columnEncoder()->encode(column); +} + +std::string encodeExtra(const ColumnEncoderContext & encoderContext, ColumnEncoder & extraEncoder, const std::string & value) +{ + ScopedColumnEncoderContext scopedContext(encoderContext, extraEncoder); + return extraEncoder.encode(value); +} + +std::string payloadJson(const ColumnEncoderContext & encoderContext, ColumnEncoder & extraEncoder, + const std::string & firstColumn, + const std::string & secondColumn, + const std::string & extraValue) +{ + const std::string encodedFirstColumn = encodeColumn(encoderContext, extraEncoder, firstColumn); + const std::string encodedSecondColumn = encodeColumn(encoderContext, extraEncoder, secondColumn); + const std::string encodedExtraValue = encodeExtra(encoderContext, extraEncoder, extraValue); + + Json::Value payload(Json::objectValue); + payload[encodedFirstColumn] = encodedExtraValue; + payload[encodedExtraValue] = encodedFirstColumn; + payload["nested"][0] = encodedSecondColumn; + payload["nested"][1]["label"] = std::string("label:") + encodedExtraValue; + payload["nested"][1]["formula"] = encodedFirstColumn + " + " + encodedSecondColumn; + return payload.toStyledString(); +} + +Json::Value decodePayload(const std::string & payload, const std::string & encoderContextJson, ColumnEncoder & extraEncoder) +{ + return decodeColumnJson(payload.c_str(), encoderContextJson.c_str(), extraEncoder); +} + +void verifyDecodedPayload(const Json::Value & decoded, + const std::string & firstColumn, + const std::string & secondColumn, + const std::string & extraValue) +{ + QVERIFY2(decoded.isObject(), "Decoded payload should be a JSON object."); + QVERIFY2(decoded.isMember(firstColumn), "Encoded object member name was not decoded with the expected dataset column context."); + QVERIFY2(decoded.isMember(extraValue), "Encoded extra-option member name was not decoded with the expected extra-option context."); + QCOMPARE(QString::fromStdString(decoded[firstColumn].asString()), QString::fromStdString(extraValue)); + QCOMPARE(QString::fromStdString(decoded[extraValue].asString()), QString::fromStdString(firstColumn)); + QCOMPARE(QString::fromStdString(decoded["nested"][0].asString()), QString::fromStdString(secondColumn)); + QCOMPARE(QString::fromStdString(decoded["nested"][1]["label"].asString()), QString::fromStdString("label:" + extraValue)); + QCOMPARE(QString::fromStdString(decoded["nested"][1]["formula"].asString()), QString::fromStdString(firstColumn + " + " + secondColumn)); +} + +std::string captureError(const std::function & function) +{ + try + { + function(); + } + catch(const std::exception & exception) + { + return exception.what(); + } + + return ""; +} +} + +void TestColumnEncoderContext::init() +{ + ColumnEncoder::columnEncoder()->setCurrentNames(emptyNames()); +} + +void TestColumnEncoderContext::cleanup() +{ + ColumnEncoder::columnEncoder()->setCurrentNames(emptyNames()); +} + +void TestColumnEncoderContext::scopedContextsDecodeInterleavedAndRestoreLiveState() +{ + ColumnEncoder extraEncoder(ExtraOptionsPrefix); + + const ColumnEncoderContext liveContext = context( + names({{"live group", columnType::nominal}, {"live score", columnType::scale}}), + names({{"live factor A", columnType::unknown}, {"live factor B", columnType::unknown}}) + ); + + activateContext(liveContext, extraEncoder); + + const std::string livePayload = payloadJson(liveContext, extraEncoder, "live group", "live score", "live factor A"); + + const ColumnEncoderContext firstArchiveContext = context( + names({{"archive A group", columnType::nominal}, {"archive A score", columnType::scale}}), + names({{"archive A level", columnType::unknown}}) + ); + const ColumnEncoderContext secondArchiveContext = context( + names({{"archive B group", columnType::ordinal}, {"archive B score", columnType::scale}}), + names({{"archive B level", columnType::unknown}}) + ); + const ColumnEncoderContext thirdArchiveContext = context( + names({{"archive C group", columnType::nominal}, {"archive C score", columnType::scale}}), + names({{"archive C level", columnType::unknown}}) + ); + + const std::string firstArchivePayload = payloadJson(firstArchiveContext, extraEncoder, "archive A group", "archive A score", "archive A level"); + const std::string secondArchivePayload = payloadJson(secondArchiveContext, extraEncoder, "archive B group", "archive B score", "archive B level"); + const std::string thirdArchivePayload = payloadJson(thirdArchiveContext, extraEncoder, "archive C group", "archive C score", "archive C level"); + + verifyDecodedPayload(decodePayload(livePayload, "", extraEncoder), "live group", "live score", "live factor A"); + + verifyDecodedPayload(decodePayload(firstArchivePayload, contextJson(firstArchiveContext), extraEncoder), "archive A group", "archive A score", "archive A level"); + verifyDecodedPayload(decodePayload(livePayload, "", extraEncoder), "live group", "live score", "live factor A"); + + verifyDecodedPayload(decodePayload(secondArchivePayload, contextJson(secondArchiveContext), extraEncoder), "archive B group", "archive B score", "archive B level"); + verifyDecodedPayload(decodePayload(livePayload, "", extraEncoder), "live group", "live score", "live factor A"); + + verifyDecodedPayload(decodePayload(thirdArchivePayload, contextJson(thirdArchiveContext), extraEncoder), "archive C group", "archive C score", "archive C level"); + verifyDecodedPayload(decodePayload(livePayload, "", extraEncoder), "live group", "live score", "live factor A"); + + verifyDecodedPayload(decodePayload(firstArchivePayload, contextJson(firstArchiveContext), extraEncoder), "archive A group", "archive A score", "archive A level"); + verifyDecodedPayload(decodePayload(thirdArchivePayload, contextJson(thirdArchiveContext), extraEncoder), "archive C group", "archive C score", "archive C level"); + verifyDecodedPayload(decodePayload(secondArchivePayload, contextJson(secondArchiveContext), extraEncoder), "archive B group", "archive B score", "archive B level"); + + verifyDecodedPayload(decodePayload(livePayload, "", extraEncoder), "live group", "live score", "live factor A"); + QVERIFY2(ColumnEncoder::columnEncoder()->currentNames() == liveContext.columns(), "Dataset column context was not restored after scoped decodes."); + QVERIFY2(extraEncoder.currentNames() == liveContext.extra(), "Extra-option context was not restored after scoped decodes."); +} + +void TestColumnEncoderContext::malformedContextDoesNotMutateLiveState() +{ + ColumnEncoder extraEncoder(ExtraOptionsPrefix); + + const ColumnEncoderContext liveContext = context( + names({{"live group", columnType::nominal}, {"live score", columnType::scale}}), + names({{"live factor", columnType::unknown}}) + ); + + activateContext(liveContext, extraEncoder); + + const std::string livePayload = payloadJson(liveContext, extraEncoder, "live group", "live score", "live factor"); + + const std::string parseError = captureError([&]() + { + decodePayload(livePayload, "{not-json", extraEncoder); + }); + const std::string parseErrorMessage = "Unexpected parse error: " + parseError; + QVERIFY2(parseError.find("Could not parse column encoder context JSON.") != std::string::npos, + parseErrorMessage.c_str()); + verifyDecodedPayload(decodePayload(livePayload, "", extraEncoder), "live group", "live score", "live factor"); + + const std::string schemaError = captureError([&]() + { + decodePayload(livePayload, "{\"version\":1,\"columns\":{},\"extra\":[]}", extraEncoder); + }); + const std::string schemaErrorMessage = "Unexpected schema error: " + schemaError; + QVERIFY2(schemaError.find("Column encoder context field 'columns' must be an array.") != std::string::npos, + schemaErrorMessage.c_str()); + verifyDecodedPayload(decodePayload(livePayload, "", extraEncoder), "live group", "live score", "live factor"); + + QVERIFY2(ColumnEncoder::columnEncoder()->currentNames() == liveContext.columns(), "Dataset column context changed after malformed context errors."); + QVERIFY2(extraEncoder.currentNames() == liveContext.extra(), "Extra-option context changed after malformed context errors."); +} + +QTEST_MAIN(TestColumnEncoderContext) diff --git a/Tests/testcolumnencodercontext.h b/Tests/testcolumnencodercontext.h new file mode 100644 index 0000000000..a3a268ce30 --- /dev/null +++ b/Tests/testcolumnencodercontext.h @@ -0,0 +1,35 @@ +// +// Copyright (C) 2026 University of Amsterdam +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public +// License along with this program. If not, see +// . +// + +#ifndef TESTCOLUMNENCODERCONTEXT_H +#define TESTCOLUMNENCODERCONTEXT_H + +#include + +class TestColumnEncoderContext : public QObject +{ + Q_OBJECT + +private slots: + void init(); + void cleanup(); + void scopedContextsDecodeInterleavedAndRestoreLiveState(); + void malformedContextDoesNotMutateLiveState(); +}; + +#endif // TESTCOLUMNENCODERCONTEXT_H