Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions CommHandler/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,18 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased
## [unreleased]

## Added

- client for SGr declaration library
- JSONata expressions as response query in REST / messaging data points and message filters
- response or template query can be used in write operations

### Changed

- requires updated driver implementations
- requires updated driver implementations supporting driver API 2.4
- requires `sgr-specification` 2.2


## [2.4.2] - 2025-10-09
Expand Down
1 change: 1 addition & 0 deletions CommHandler/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ dependencies {
api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.18.4'
implementation group: 'io.burt', name: 'jmespath-core', version: '0.6.0'
implementation group: 'io.burt', name: 'jmespath-jackson', version: '0.6.0'
implementation group: 'com.dashjoin', name: 'jsonata', version: '0.9.8'
implementation group: 'commons-io', name: 'commons-io', version: '2.15.1'
implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.19.0'
implementation group: 'jakarta.annotation', name: 'jakarta.annotation-api', version: '2.1.1'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.smartgridready.communicator.common.helper;

import com.dashjoin.jsonata.Jsonata;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
Expand Down Expand Up @@ -55,6 +56,32 @@ public static Value parseJsonResponse(String jmesPath, String jsonResp) throws G
JsonNode res = expression.search(jsonNode);
return JsonValue.of(res);
}
} catch (Exception e) {
throw new GenDriverException("Failed to parse JSON response", e);
}
}

/**
* Evaluates a JSONata expression on a JSON string and returns as SGr value.
* @param jsonataExpression the JSONata expression
* @param jsonResp the JSON string
* @return an instance of {@link JsonValue}
* @throws GenDriverException when an error occurred during parsing
*/
public static Value parseJsonResponseWithJsonata(String jsonataExpression, String jsonResp) throws GenDriverException {

ObjectMapper mapper = new ObjectMapper();

try {
if (jsonataExpression != null && !jsonataExpression.trim().isEmpty()) {
var expression = Jsonata.jsonata(jsonataExpression);
var jsonObj = mapper.readValue(jsonResp, Object.class);
var result = expression.evaluate(jsonObj);
return JsonValue.of(result);
}

return JsonValue.of(mapper.readTree(jsonResp));

} catch (IOException e) {
throw new GenDriverException("Failed to parse JSON response", e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ public boolean isFilterMatch(String payload) {
} catch (GenDriverException e) {
return false; // no match
}
} else if (messageFilter.getJsonataFilter() != null) {
try {
var filter = messageFilter.getJsonataFilter();
regexMatch = filter.getMatchesRegex();
payloadValue = JsonHelper.parseJsonResponseWithJsonata(filter.getQuery(), payload);
} catch (GenDriverException e) {
return false; // no match
}
}

// regex matching for all filter types
Expand All @@ -75,7 +83,8 @@ public void validate() throws OperationNotSupportedException {
(messageFilter.getPlaintextFilter() != null) ||
(messageFilter.getRegexFilter() != null) ||
(messageFilter.getXpapathFilter() != null) ||
(messageFilter.getJmespathFilter() != null)
(messageFilter.getJmespathFilter() != null) ||
(messageFilter.getJsonataFilter() != null)
) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public class SGrMessagingDevice extends SGrDeviceBase<
MessagingFunctionalProfile,
MessagingDataPoint> implements GenDeviceApi4Messaging {

@SuppressWarnings("unused")
private static final Logger LOG = LoggerFactory.getLogger(SGrMessagingDevice.class);

private static final long SYNC_READ_TIMEOUT_MSEC = 60000;
Expand Down Expand Up @@ -208,30 +209,14 @@ private Value getValueFromDevice(Map<String, String> parameters, long timeoutMs,
.get() // returns Message
.getPayload();

Optional<ResponseQuery> queryOpt = Optional.ofNullable(dataPoint.getMessagingDataPointConfiguration())
.map(MessagingDataPointConfiguration::getInMessage)
.map(InMessage::getResponseQuery);

Value value;
if (queryOpt.isPresent()) {
ResponseQuery responseQuery = queryOpt.get();
if (responseQuery.getQueryType() != null && ResponseQueryType.JMES_PATH_EXPRESSION == responseQuery.getQueryType()) {
value = JsonHelper.parseJsonResponse(responseQuery.getQuery(), response);
} else if (responseQuery.getQueryType() != null && ResponseQueryType.JMES_PATH_MAPPING == responseQuery.getQueryType()) {
value = JsonHelper.mapJsonResponse(responseQuery.getJmesPathMappings(), response);
} else if (responseQuery.getQueryType() != null && ResponseQueryType.X_PATH_EXPRESSION == responseQuery.getQueryType()) {
value = XPathHelper.parseXmlResponse(responseQuery.getQuery(), response);
} else if (responseQuery.getQueryType() != null && ResponseQueryType.REGULAR_EXPRESSION == responseQuery.getQueryType()) {
value = RegexHelper.query(responseQuery.getQuery(), response);
} else if (responseQuery.getQueryType() != null) {
throw new GenDriverException("Response query type " + responseQuery.getQueryType().name() + " not supported yet");
} else {
throw new GenDriverException("Response query type missing");
}
} else {
// mapping device -> generic (only for plain string values)
value = getMappedGenericValue(dataPoint.getMessagingDataPointConfiguration(), response);
}

Value value = StringValue.of(response);

// value transformation using responseQuery
value = getTransformedGenericValue(dataPoint.getMessagingDataPointConfiguration(), value);

// mapping device -> generic (only for plain string values)
value = getMappedGenericValue(dataPoint.getMessagingDataPointConfiguration(), value);

// unit conversion before returning to client
return applyUnitConversion(dataPoint, value, SGrDeviceBase::multiply);
Expand Down Expand Up @@ -264,10 +249,13 @@ public void setVal(String profileName, String dataPointName, Value value)
value = applyUnitConversion(dataPoint, value, SGrDeviceBase::divide);

// mapping generic -> device
String outValue = getMappedDeviceValue(dataPoint.getMessagingDataPointConfiguration(), value);
value = getMappedDeviceValue(dataPoint.getMessagingDataPointConfiguration(), value);

// value transformation using templateQuery
value = getTransformedDeviceValue(dataPoint.getMessagingDataPointConfiguration(), value);

// no regex here, string literal replacement is sufficient
outMessageTemplate = outMessageTemplate.replace("[[value]]", outValue);
outMessageTemplate = outMessageTemplate.replace("[[value]]", value.getString());

messagingClient.sendSync(outMessageTopic, Message.of(outMessageTemplate));
}
Expand Down Expand Up @@ -308,35 +296,14 @@ private void transformIncomingMessage(

String response = Optional.ofNullable(msgReceiveResult.get().getPayload()).orElse("");

Optional<ResponseQuery> queryOpt = Optional.ofNullable(dataPoint.getMessagingDataPointConfiguration())
.map(MessagingDataPointConfiguration::getInMessage)
.map(InMessage::getResponseQuery);

try {
Value value;
if (queryOpt.isPresent()) {
switch (queryOpt.get().getQueryType()) {
case JMES_PATH_EXPRESSION:
value = JsonHelper.parseJsonResponse(queryOpt.get().getQuery(), response);
break;
case JMES_PATH_MAPPING:
value = JsonHelper.mapJsonResponse(queryOpt.get().getJmesPathMappings(), response);
break;
case X_PATH_EXPRESSION:
value = XPathHelper.parseXmlResponse(queryOpt.get().getQuery(), response);
break;
case REGULAR_EXPRESSION:
value = RegexHelper.query(queryOpt.get().getQuery(), response);
break;
default:
throw new GenDriverException("Response query type " + queryOpt.get().getQueryType().name() + " not supported yet");
}
} else {
// mapping device -> generic (only for plain string values)
value = getMappedGenericValue(dataPoint.getMessagingDataPointConfiguration(), response);
}
LOG.debug("Received subscribed message on topic={}, filter={}, payload={}",
inMessageTopic, queryOpt.isPresent() ? queryOpt.get().getQuery() : "none", response);
Value value = StringValue.of(response);

// value transformation using responseQuery
value = getTransformedGenericValue(dataPoint.getMessagingDataPointConfiguration(), value);

// mapping device -> generic (only for plain string values)
value = getMappedGenericValue(dataPoint.getMessagingDataPointConfiguration(), value);

// unit conversion before inserting into cache
value = applyUnitConversion(dataPoint, value, SGrDeviceBase::multiply);
Expand Down Expand Up @@ -425,40 +392,106 @@ private int countCacheRecords4Topic(String topic) {
return count.get();
}

private static Value getMappedGenericValue(MessagingDataPointConfiguration dataPointConfiguration, String value) {
String mappedValue = value;
private static Value getMappedGenericValue(MessagingDataPointConfiguration dataPointConfiguration, Value value) {
Value mappedValue = value;

List<ValueMapping> valueMappings = Optional.ofNullable(dataPointConfiguration)
.map(MessagingDataPointConfiguration::getInMessage)
.map(InMessage::getValueMapping)
.map(MessagingValueMapping::getMapping).orElse(Collections.emptyList());
for (ValueMapping mapping: valueMappings) {
if (mappedValue.equals(mapping.getDeviceValue())) {
mappedValue = mapping.getGenericValue();
break;
}
.map(MessagingValueMapping::getMapping)
.orElse(Collections.emptyList());

final String strVal = mappedValue.getString();
Optional<ValueMapping> mappingOpt = valueMappings.stream()
.filter(m -> strVal.equals(m.getDeviceValue()))
.findFirst();
if (mappingOpt.isPresent()) {
mappedValue = StringValue.of(mappingOpt.get().getGenericValue());
}

return StringValue.of(mappedValue);
return mappedValue;
}

private static String getMappedDeviceValue(MessagingDataPointConfiguration dataPointConfiguration, Value value) {
String mappedValue = value.getString();
private static Value getMappedDeviceValue(MessagingDataPointConfiguration dataPointConfiguration, Value value) {
Value mappedValue = value;

List<ValueMapping> valueMappings = Optional.ofNullable(dataPointConfiguration)
.map(MessagingDataPointConfiguration::getWriteCmdMessage)
.map(OutMessage::getValueMapping)
.map(MessagingValueMapping::getMapping).orElse(Collections.emptyList());
for (ValueMapping mapping: valueMappings) {
if (mappedValue.equals(mapping.getGenericValue())) {
mappedValue = mapping.getDeviceValue();
break;
}
.map(MessagingValueMapping::getMapping)
.orElse(Collections.emptyList());

final String strVal = mappedValue.getString();
Optional<ValueMapping> mappingOpt = valueMappings.stream()
.filter(m -> strVal.equals(m.getGenericValue()))
.findFirst();
if (mappingOpt.isPresent()) {
mappedValue = StringValue.of(mappingOpt.get().getDeviceValue());
}

return mappedValue;
}

private static Value getTransformedGenericValue(MessagingDataPointConfiguration dataPointConfiguration, Value value) throws GenDriverException {
Value tmpValue = value;

ResponseQuery responseQuery = Optional.ofNullable(dataPointConfiguration)
.map(MessagingDataPointConfiguration::getInMessage)
.map(InMessage::getResponseQuery)
.orElse(null);

if (responseQuery != null) {
if (responseQuery.getQueryType() == null) {
throw new GenDriverException("Response query type missing");
}
if (ResponseQueryType.JMES_PATH_EXPRESSION == responseQuery.getQueryType()) {
tmpValue = JsonHelper.parseJsonResponse(responseQuery.getQuery(), tmpValue.getString());
} else if (ResponseQueryType.JMES_PATH_MAPPING == responseQuery.getQueryType()) {
tmpValue = JsonHelper.mapJsonResponse(responseQuery.getJmesPathMappings(), tmpValue.getString());
} else if (ResponseQueryType.X_PATH_EXPRESSION == responseQuery.getQueryType()) {
tmpValue = XPathHelper.parseXmlResponse(responseQuery.getQuery(), tmpValue.getString());
} else if (ResponseQueryType.REGULAR_EXPRESSION == responseQuery.getQueryType()) {
tmpValue = RegexHelper.query(responseQuery.getQuery(), tmpValue.getString());
} else if (ResponseQueryType.JSO_NATA_EXPRESSION == responseQuery.getQueryType()) {
tmpValue = JsonHelper.parseJsonResponseWithJsonata(responseQuery.getQuery(), tmpValue.getString());
} else {
throw new GenDriverException("Response query type " + responseQuery.getQueryType().name() + " not supported yet");
}
}

return tmpValue;
}

private static Value getTransformedDeviceValue(MessagingDataPointConfiguration dataPointConfiguration, Value value) throws GenDriverException {
Value tmpValue = value;

ResponseQuery templateQuery = Optional.ofNullable(dataPointConfiguration)
.map(MessagingDataPointConfiguration::getWriteCmdMessage)
.map(OutMessage::getTemplateQuery)
.orElse(null);

if (templateQuery != null) {
if (templateQuery.getQueryType() == null) {
throw new GenDriverException("Template query type missing");
}
if (ResponseQueryType.JMES_PATH_EXPRESSION == templateQuery.getQueryType()) {
tmpValue = JsonHelper.parseJsonResponse(templateQuery.getQuery(), tmpValue.getString());
} else if (ResponseQueryType.JMES_PATH_MAPPING == templateQuery.getQueryType()) {
tmpValue = JsonHelper.mapJsonResponse(templateQuery.getJmesPathMappings(), tmpValue.getString());
} else if (ResponseQueryType.X_PATH_EXPRESSION == templateQuery.getQueryType()) {
tmpValue = XPathHelper.parseXmlResponse(templateQuery.getQuery(), tmpValue.getString());
} else if (ResponseQueryType.REGULAR_EXPRESSION == templateQuery.getQueryType()) {
tmpValue = RegexHelper.query(templateQuery.getQuery(), tmpValue.getString());
} else if (ResponseQueryType.JSO_NATA_EXPRESSION == templateQuery.getQueryType()) {
tmpValue = JsonHelper.parseJsonResponseWithJsonata(templateQuery.getQuery(), tmpValue.getString());
} else {
throw new GenDriverException("Template query type " + templateQuery.getQueryType().name() + " not supported yet");
}
}

return tmpValue;
}

private static String substituteParameterPlaceholders(String template, Map<String, String> substitutions) {
// this is for dynamic parameters
String convertedTemplate = template;
Expand Down
Loading