/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. * * Copyright 2018 (c) basysKom GmbH (Author: Peter Rustler) */ #include #include #include typedef struct { UA_HistoryDataGathering gathering; } UA_HistoryDatabaseContext_default; static size_t getResultSize_service_default(const UA_HistoryDataBackend* backend, UA_Server *server, const UA_NodeId *sessionId, void* sessionContext, const UA_NodeId *nodeId, UA_DateTime start, UA_DateTime end, UA_UInt32 numValuesPerNode, UA_Boolean returnBounds, size_t *startIndex, size_t *endIndex, UA_Boolean *addFirst, UA_Boolean *addLast, UA_Boolean *reverse) { size_t storeEnd = backend->getEnd(server, backend->context, sessionId, sessionContext, nodeId); size_t firstIndex = backend->firstIndex(server, backend->context, sessionId, sessionContext, nodeId); size_t lastIndex = backend->lastIndex(server, backend->context, sessionId, sessionContext, nodeId); *startIndex = storeEnd; *endIndex = storeEnd; *addFirst = false; *addLast = false; if (end == LLONG_MIN) { *reverse = false; } else if (start == LLONG_MIN) { *reverse = true; } else { *reverse = end < start; } UA_Boolean equal = start == end; size_t size = 0; if (lastIndex != storeEnd) { if (equal) { if (returnBounds) { *startIndex = backend->getDateTimeMatch(server, backend->context, sessionId, sessionContext, nodeId, start, MATCH_EQUAL_OR_BEFORE); if (*startIndex == storeEnd) { *startIndex = backend->getDateTimeMatch(server, backend->context, sessionId, sessionContext, nodeId, start, MATCH_AFTER); *addFirst = true; } *endIndex = backend->getDateTimeMatch(server, backend->context, sessionId, sessionContext, nodeId, start, MATCH_AFTER); size = backend->resultSize(server, backend->context, sessionId, sessionContext, nodeId, *startIndex, *endIndex); } else { *startIndex = backend->getDateTimeMatch(server, backend->context, sessionId, sessionContext, nodeId, start, MATCH_EQUAL); *endIndex = *startIndex; if (*startIndex == storeEnd) size = 0; else size = 1; } } else if (start == LLONG_MIN) { *endIndex = firstIndex; if (returnBounds) { *addLast = true; *startIndex = backend->getDateTimeMatch(server, backend->context, sessionId, sessionContext, nodeId, end, MATCH_EQUAL_OR_AFTER); if (*startIndex == storeEnd) { *startIndex = backend->getDateTimeMatch(server, backend->context, sessionId, sessionContext, nodeId, end, MATCH_EQUAL_OR_BEFORE); *addFirst = true; } } else { *startIndex = backend->getDateTimeMatch(server, backend->context, sessionId, sessionContext, nodeId, end, MATCH_EQUAL_OR_BEFORE); } size = backend->resultSize(server, backend->context, sessionId, sessionContext, nodeId, *endIndex, *startIndex); } else if (end == LLONG_MIN) { *endIndex = lastIndex; if (returnBounds) { *addLast = true; *startIndex = backend->getDateTimeMatch(server, backend->context, sessionId, sessionContext, nodeId, start, MATCH_EQUAL_OR_BEFORE); if (*startIndex == storeEnd) { *startIndex = backend->getDateTimeMatch(server, backend->context, sessionId, sessionContext, nodeId, start, MATCH_AFTER); *addFirst = true; } } else { *startIndex = backend->getDateTimeMatch(server, backend->context, sessionId, sessionContext, nodeId, start, MATCH_EQUAL_OR_AFTER); } size = backend->resultSize(server, backend->context, sessionId, sessionContext, nodeId, *startIndex, *endIndex); } else if (*reverse) { if (returnBounds) { *startIndex = backend->getDateTimeMatch(server, backend->context, sessionId, sessionContext, nodeId, start, MATCH_EQUAL_OR_AFTER); if (*startIndex == storeEnd) { *addFirst = true; *startIndex = backend->getDateTimeMatch(server, backend->context, sessionId, sessionContext, nodeId, start, MATCH_BEFORE); } *endIndex = backend->getDateTimeMatch(server, backend->context, sessionId, sessionContext, nodeId, end, MATCH_EQUAL_OR_BEFORE); if (*endIndex == storeEnd) { *addLast = true; *endIndex = backend->getDateTimeMatch(server, backend->context, sessionId, sessionContext, nodeId, end, MATCH_AFTER); } } else { *startIndex = backend->getDateTimeMatch(server, backend->context, sessionId, sessionContext, nodeId, start, MATCH_EQUAL_OR_BEFORE); *endIndex = backend->getDateTimeMatch(server, backend->context, sessionId, sessionContext, nodeId, end, MATCH_AFTER); } size = backend->resultSize(server, backend->context, sessionId, sessionContext, nodeId, *endIndex, *startIndex); } else { if (returnBounds) { *startIndex = backend->getDateTimeMatch(server, backend->context, sessionId, sessionContext, nodeId, start, MATCH_EQUAL_OR_BEFORE); if (*startIndex == storeEnd) { *addFirst = true; *startIndex = backend->getDateTimeMatch(server, backend->context, sessionId, sessionContext, nodeId, start, MATCH_AFTER); } *endIndex = backend->getDateTimeMatch(server, backend->context, sessionId, sessionContext, nodeId, end, MATCH_EQUAL_OR_AFTER); if (*endIndex == storeEnd) { *addLast = true; *endIndex = backend->getDateTimeMatch(server, backend->context, sessionId, sessionContext, nodeId, end, MATCH_BEFORE); } } else { *startIndex = backend->getDateTimeMatch(server, backend->context, sessionId, sessionContext, nodeId, start, MATCH_EQUAL_OR_AFTER); *endIndex = backend->getDateTimeMatch(server, backend->context, sessionId, sessionContext, nodeId, end, MATCH_BEFORE); } size = backend->resultSize(server, backend->context, sessionId, sessionContext, nodeId, *startIndex, *endIndex); } } else if (returnBounds) { *addLast = true; *addFirst = true; } if (*addLast) ++size; if (*addFirst) ++size; if (numValuesPerNode > 0 && size > numValuesPerNode) { size = numValuesPerNode; *addLast = false; } return size; } static UA_StatusCode getHistoryData_service_default(const UA_HistoryDataBackend* backend, const UA_DateTime start, const UA_DateTime end, UA_Server *server, const UA_NodeId *sessionId, void *sessionContext, const UA_NodeId* nodeId, size_t maxSize, UA_UInt32 numValuesPerNode, UA_Boolean returnBounds, UA_TimestampsToReturn timestampsToReturn, UA_NumericRange range, UA_Boolean releaseContinuationPoints, const UA_ByteString *continuationPoint, UA_ByteString *outContinuationPoint, size_t *resultSize, UA_DataValue ** result) { size_t skip = 0; UA_ByteString backendContinuationPoint; UA_ByteString_init(&backendContinuationPoint); if (continuationPoint->length > 0) { if (continuationPoint->length >= sizeof(size_t)) { skip = *((size_t*)(continuationPoint->data)); if (continuationPoint->length > 0) { backendContinuationPoint.length = continuationPoint->length - sizeof(size_t); backendContinuationPoint.data = continuationPoint->data + sizeof(size_t); } } else { return UA_STATUSCODE_BADCONTINUATIONPOINTINVALID; } } size_t storeEnd = backend->getEnd(server, backend->context, sessionId, sessionContext, nodeId); size_t startIndex; size_t endIndex; UA_Boolean addFirst; UA_Boolean addLast; UA_Boolean reverse; size_t _resultSize = getResultSize_service_default(backend, server, sessionId, sessionContext, nodeId, start, end, numValuesPerNode == 0 ? 0 : numValuesPerNode + (UA_UInt32)skip, returnBounds, &startIndex, &endIndex, &addFirst, &addLast, &reverse); *resultSize = _resultSize - skip; if (*resultSize > maxSize) { *resultSize = maxSize; } UA_DataValue *outResult= (UA_DataValue*)UA_Array_new(*resultSize, &UA_TYPES[UA_TYPES_DATAVALUE]); if (!outResult) { *resultSize = 0; return UA_STATUSCODE_BADOUTOFMEMORY; } *result = outResult; size_t counter = 0; if (addFirst) { if (skip == 0) { outResult[counter].hasStatus = true; outResult[counter].status = UA_STATUSCODE_BADBOUNDNOTFOUND; outResult[counter].hasSourceTimestamp = true; if (start == LLONG_MIN) { outResult[counter].sourceTimestamp = end; } else { outResult[counter].sourceTimestamp = start; } ++counter; } } UA_ByteString backendOutContinuationPoint; UA_ByteString_init(&backendOutContinuationPoint); if (endIndex != storeEnd && startIndex != storeEnd) { size_t retval = 0; size_t valueSize = *resultSize - counter; if (valueSize + skip > _resultSize - addFirst - addLast) { if (skip == 0) { valueSize = _resultSize - addFirst - addLast; } else { valueSize = _resultSize - skip - addLast; } } UA_StatusCode ret = UA_STATUSCODE_GOOD; if (valueSize > 0) ret = backend->copyDataValues(server, backend->context, sessionId, sessionContext, nodeId, startIndex, endIndex, reverse, valueSize, range, releaseContinuationPoints, &backendContinuationPoint, &backendOutContinuationPoint, &retval, &outResult[counter]); if (ret != UA_STATUSCODE_GOOD) { UA_Array_delete(outResult, *resultSize, &UA_TYPES[UA_TYPES_DATAVALUE]); *result = NULL; *resultSize = 0; return ret; } counter += retval; } if (addLast && counter < *resultSize) { outResult[counter].hasStatus = true; outResult[counter].status = UA_STATUSCODE_BADBOUNDNOTFOUND; outResult[counter].hasSourceTimestamp = true; if (start == LLONG_MIN && storeEnd != backend->firstIndex(server, backend->context, sessionId, sessionContext, nodeId)) { outResult[counter].sourceTimestamp = backend->getDataValue(server, backend->context, sessionId, sessionContext, nodeId, endIndex)->sourceTimestamp - UA_DATETIME_SEC; } else if (end == LLONG_MIN && storeEnd != backend->firstIndex(server, backend->context, sessionId, sessionContext, nodeId)) { outResult[counter].sourceTimestamp = backend->getDataValue(server, backend->context, sessionId, sessionContext, nodeId, endIndex)->sourceTimestamp + UA_DATETIME_SEC; } else { outResult[counter].sourceTimestamp = end; } } // there are more values if (skip + *resultSize < _resultSize // there are not more values for this request, but there are more values in database || (backendOutContinuationPoint.length > 0 && numValuesPerNode != 0) // we deliver just one value which is a FIRST/LAST value || (skip == 0 && addFirst == true && *resultSize == 1)) { if(UA_ByteString_allocBuffer(outContinuationPoint, backendOutContinuationPoint.length + sizeof(size_t)) != UA_STATUSCODE_GOOD) { return UA_STATUSCODE_BADOUTOFMEMORY; } *((size_t*)(outContinuationPoint->data)) = skip + *resultSize; if(backendOutContinuationPoint.length > 0) memcpy(outContinuationPoint->data + sizeof(size_t), backendOutContinuationPoint.data, backendOutContinuationPoint.length); } UA_ByteString_clear(&backendOutContinuationPoint); return UA_STATUSCODE_GOOD; } static void updateData_service_default(UA_Server *server, void *hdbContext, const UA_NodeId *sessionId, void *sessionContext, const UA_RequestHeader *requestHeader, const UA_UpdateDataDetails *details, UA_HistoryUpdateResult *result) { UA_HistoryDatabaseContext_default *ctx = (UA_HistoryDatabaseContext_default*)hdbContext; UA_Byte accessLevel = 0; UA_Server_readAccessLevel(server, details->nodeId, &accessLevel); if (!(accessLevel & UA_ACCESSLEVELMASK_HISTORYWRITE)) { result->statusCode = UA_STATUSCODE_BADUSERACCESSDENIED; return; } UA_Boolean historizing = false; UA_Server_readHistorizing(server, details->nodeId, &historizing); if (!historizing) { result->statusCode = UA_STATUSCODE_BADHISTORYOPERATIONINVALID; return; } const UA_HistorizingNodeIdSettings *setting = ctx->gathering.getHistorizingSetting( server, ctx->gathering.context, &details->nodeId); if (!setting) { result->statusCode = UA_STATUSCODE_BADHISTORYOPERATIONINVALID; return; } UA_ServerConfig *config = UA_Server_getConfig(server); result->operationResultsSize = details->updateValuesSize; result->operationResults = (UA_StatusCode*)UA_Array_new(result->operationResultsSize, &UA_TYPES[UA_TYPES_STATUSCODE]); for (size_t i = 0; i < details->updateValuesSize; ++i) { if (config->accessControl.allowHistoryUpdateUpdateData && !config->accessControl.allowHistoryUpdateUpdateData(server, &config->accessControl, sessionId, sessionContext, &details->nodeId, details->performInsertReplace, &details->updateValues[i])) { result->operationResults[i] = UA_STATUSCODE_BADUSERACCESSDENIED; continue; } switch (details->performInsertReplace) { case UA_PERFORMUPDATETYPE_INSERT: if (!setting->historizingBackend.insertDataValue) { result->operationResults[i] = UA_STATUSCODE_BADHISTORYOPERATIONUNSUPPORTED; continue; } result->operationResults[i] = setting->historizingBackend.insertDataValue(server, setting->historizingBackend.context, sessionId, sessionContext, &details->nodeId, &details->updateValues[i]); continue; case UA_PERFORMUPDATETYPE_REPLACE: if (!setting->historizingBackend.replaceDataValue) { result->operationResults[i] = UA_STATUSCODE_BADHISTORYOPERATIONUNSUPPORTED; continue; } result->operationResults[i] = setting->historizingBackend.replaceDataValue(server, setting->historizingBackend.context, sessionId, sessionContext, &details->nodeId, &details->updateValues[i]); continue; case UA_PERFORMUPDATETYPE_UPDATE: if (!setting->historizingBackend.updateDataValue) { result->operationResults[i] = UA_STATUSCODE_BADHISTORYOPERATIONUNSUPPORTED; continue; } result->operationResults[i] = setting->historizingBackend.updateDataValue(server, setting->historizingBackend.context, sessionId, sessionContext, &details->nodeId, &details->updateValues[i]); continue; default: result->operationResults[i] = UA_STATUSCODE_BADHISTORYOPERATIONINVALID; continue; } } } static void deleteRawModified_service_default(UA_Server *server, void *hdbContext, const UA_NodeId *sessionId, void *sessionContext, const UA_RequestHeader *requestHeader, const UA_DeleteRawModifiedDetails *details, UA_HistoryUpdateResult *result) { if (details->isDeleteModified) { result->statusCode = UA_STATUSCODE_BADHISTORYOPERATIONUNSUPPORTED; return; } UA_HistoryDatabaseContext_default *ctx = (UA_HistoryDatabaseContext_default*)hdbContext; UA_Byte accessLevel = 0; UA_Server_readAccessLevel(server, details->nodeId, &accessLevel); if (!(accessLevel & UA_ACCESSLEVELMASK_HISTORYWRITE)) { result->statusCode = UA_STATUSCODE_BADUSERACCESSDENIED; return; } UA_Boolean historizing = false; UA_Server_readHistorizing(server, details->nodeId, &historizing); if (!historizing) { result->statusCode = UA_STATUSCODE_BADHISTORYOPERATIONINVALID; return; } const UA_HistorizingNodeIdSettings *setting = ctx->gathering.getHistorizingSetting( server, ctx->gathering.context, &details->nodeId); if (!setting) { result->statusCode = UA_STATUSCODE_BADHISTORYOPERATIONINVALID; return; } if (!setting->historizingBackend.removeDataValue) { result->statusCode = UA_STATUSCODE_BADHISTORYOPERATIONUNSUPPORTED; return; } UA_ServerConfig *config = UA_Server_getConfig(server); if (config->accessControl.allowHistoryUpdateDeleteRawModified && !config->accessControl.allowHistoryUpdateDeleteRawModified(server, &config->accessControl, sessionId, sessionContext, &details->nodeId, details->startTime, details->endTime, details->isDeleteModified)) { result->statusCode = UA_STATUSCODE_BADUSERACCESSDENIED; return; } result->statusCode = setting->historizingBackend.removeDataValue(server, setting->historizingBackend.context, sessionId, sessionContext, &details->nodeId, details->startTime, details->endTime); } static void readRaw_service_default(UA_Server *server, void *context, const UA_NodeId *sessionId, void *sessionContext, const UA_RequestHeader *requestHeader, const UA_ReadRawModifiedDetails *historyReadDetails, UA_TimestampsToReturn timestampsToReturn, UA_Boolean releaseContinuationPoints, size_t nodesToReadSize, const UA_HistoryReadValueId *nodesToRead, UA_HistoryReadResponse *response, UA_HistoryData * const * const historyData) { UA_HistoryDatabaseContext_default *ctx = (UA_HistoryDatabaseContext_default*)context; for (size_t i = 0; i < nodesToReadSize; ++i) { UA_Byte accessLevel = 0; UA_Server_readAccessLevel(server, nodesToRead[i].nodeId, &accessLevel); if (!(accessLevel & UA_ACCESSLEVELMASK_HISTORYREAD)) { response->results[i].statusCode = UA_STATUSCODE_BADUSERACCESSDENIED; continue; } UA_Boolean historizing = false; UA_Server_readHistorizing(server, nodesToRead[i].nodeId, &historizing); if (!historizing) { response->results[i].statusCode = UA_STATUSCODE_BADHISTORYOPERATIONINVALID; continue; } const UA_HistorizingNodeIdSettings *setting = ctx->gathering.getHistorizingSetting( server, ctx->gathering.context, &nodesToRead[i].nodeId); if (!setting) { response->results[i].statusCode = UA_STATUSCODE_BADHISTORYOPERATIONINVALID; continue; } if (historyReadDetails->returnBounds && !setting->historizingBackend.boundSupported( server, setting->historizingBackend.context, sessionId, sessionContext, &nodesToRead[i].nodeId)) { response->results[i].statusCode = UA_STATUSCODE_BADBOUNDNOTSUPPORTED; continue; } if (!setting->historizingBackend.timestampsToReturnSupported( server, setting->historizingBackend.context, sessionId, sessionContext, &nodesToRead[i].nodeId, timestampsToReturn)) { response->results[i].statusCode = UA_STATUSCODE_BADTIMESTAMPNOTSUPPORTED; continue; } UA_NumericRange range; range.dimensionsSize = 0; range.dimensions = NULL; if (nodesToRead[i].indexRange.length > 0) { UA_StatusCode rangeParseResult = UA_NumericRange_parse(&range, nodesToRead[i].indexRange); if (rangeParseResult != UA_STATUSCODE_GOOD) { response->results[i].statusCode = rangeParseResult; continue; } } UA_StatusCode getHistoryDataStatusCode; if (setting->historizingBackend.getHistoryData) { getHistoryDataStatusCode = setting->historizingBackend.getHistoryData( server, sessionId, sessionContext, &setting->historizingBackend, historyReadDetails->startTime, historyReadDetails->endTime, &nodesToRead[i].nodeId, setting->maxHistoryDataResponseSize, historyReadDetails->numValuesPerNode, historyReadDetails->returnBounds, timestampsToReturn, range, releaseContinuationPoints, &nodesToRead[i].continuationPoint, &response->results[i].continuationPoint, historyData[i]); } else { getHistoryDataStatusCode = getHistoryData_service_default( &setting->historizingBackend, historyReadDetails->startTime, historyReadDetails->endTime, server, sessionId, sessionContext, &nodesToRead[i].nodeId, setting->maxHistoryDataResponseSize, historyReadDetails->numValuesPerNode, historyReadDetails->returnBounds, timestampsToReturn, range, releaseContinuationPoints, &nodesToRead[i].continuationPoint, &response->results[i].continuationPoint, &historyData[i]->dataValuesSize, &historyData[i]->dataValues); } if (getHistoryDataStatusCode != UA_STATUSCODE_GOOD) { response->results[i].statusCode = getHistoryDataStatusCode; continue; } } response->responseHeader.serviceResult = UA_STATUSCODE_GOOD; return; } static void setValue_service_default(UA_Server *server, void *context, const UA_NodeId *sessionId, void *sessionContext, const UA_NodeId *nodeId, UA_Boolean historizing, const UA_DataValue *value) { UA_HistoryDatabaseContext_default *ctx = (UA_HistoryDatabaseContext_default*)context; if (ctx->gathering.setValue) ctx->gathering.setValue(server, ctx->gathering.context, sessionId, sessionContext, nodeId, historizing, value); } static void clear_service_default(UA_HistoryDatabase *hdb) { if (hdb == NULL || hdb->context == NULL) return; UA_HistoryDatabaseContext_default *ctx = (UA_HistoryDatabaseContext_default*)hdb->context; ctx->gathering.deleteMembers(&ctx->gathering); UA_free(ctx); } UA_HistoryDatabase UA_HistoryDatabase_default(UA_HistoryDataGathering gathering) { UA_HistoryDatabase hdb; memset(&hdb, 0, sizeof(UA_HistoryDatabase)); UA_HistoryDatabaseContext_default *context = (UA_HistoryDatabaseContext_default*) UA_calloc(1, sizeof(UA_HistoryDatabaseContext_default)); context->gathering = gathering; hdb.context = context; hdb.readRaw = &readRaw_service_default; hdb.setValue = &setValue_service_default; hdb.updateData = &updateData_service_default; hdb.deleteRawModified = &deleteRawModified_service_default; hdb.clear = clear_service_default; return hdb; }