feat(client-java): add writeArrayElements default-fill helper and document semantics

This commit is contained in:
Joseph Doherty
2026-06-18 03:32:20 -04:00
parent 8a1f037d5a
commit e7b8aa6114
5 changed files with 9640 additions and 436 deletions
+23
View File
@@ -124,6 +124,29 @@ the unchanged elements included. For example, to change 2 elements of a
the 2 new ones). Sending only the 2 changed values overwrites the attribute
with a 2-element array.
When only a few indices need changing and the rest should be reset to the
element type's default, use `writeArrayElements` instead of building the full
array manually:
```java
session.writeArrayElements(
serverHandle, itemHandle,
MxDataType.MX_DATA_TYPE_INTEGER,
20, // totalLength
Map.of(
2, MxValues.int32Value(42),
7, MxValues.int32Value(99)),
userId);
```
The gateway expands the sparse descriptor into a full `totalLength`-element
array before forwarding to the worker. Indices not listed in the map are
written as the element type's default — this is a **reset**, not a preserve;
current values at those positions are discarded. `totalLength` is required and
must match the declared length of the array attribute. Bare-name array items
(`Area001.Pump001.Speed`) are auto-normalized to the `[]` form at `AddItem` so
the array attribute accepts the write.
## Galaxy Repository Browse
The Galaxy Repository service is a separate metadata-only gRPC service exposed
File diff suppressed because it is too large Load Diff
@@ -4,7 +4,9 @@ import java.security.SecureRandom;
import java.time.Duration;
import java.util.HexFormat;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
import mxaccess_gateway.v1.MxaccessGateway.AddItem2Command;
import mxaccess_gateway.v1.MxaccessGateway.AddItemBulkCommand;
import mxaccess_gateway.v1.MxaccessGateway.AddItemCommand;
@@ -18,6 +20,9 @@ import mxaccess_gateway.v1.MxaccessGateway.MxCommand;
import mxaccess_gateway.v1.MxaccessGateway.MxCommandKind;
import mxaccess_gateway.v1.MxaccessGateway.MxCommandReply;
import mxaccess_gateway.v1.MxaccessGateway.MxCommandRequest;
import mxaccess_gateway.v1.MxaccessGateway.MxDataType;
import mxaccess_gateway.v1.MxaccessGateway.MxSparseArray;
import mxaccess_gateway.v1.MxaccessGateway.MxSparseElement;
import mxaccess_gateway.v1.MxaccessGateway.MxValue;
import mxaccess_gateway.v1.MxaccessGateway.OpenSessionReply;
import mxaccess_gateway.v1.MxaccessGateway.ReadBulkCommand;
@@ -603,6 +608,54 @@ public final class MxGatewaySession implements AutoCloseable {
.build());
}
/**
* Writes a subset of an array's elements using MXAccess {@code Write}, building a
* write-only {@link MxSparseArray} value that the gateway expands into a full,
* default-filled array before forwarding to the worker.
*
* <p><strong>Default-fill semantics:</strong> only the indices supplied in
* {@code elements} are written; every unmentioned index is <em>reset</em> to the
* element type's default (for example {@code 0}, {@code false}, or an empty string),
* <em>not</em> preserved from the array's current contents. Use a full
* {@link MxValue} array write when you need to keep existing element values.
*
* <p>{@code totalLength} is required and defines the length of the expanded array;
* supplied indices must be within {@code [0, totalLength)}. Elements are iterated in
* ascending index order so the produced command is deterministic.
*
* @param serverHandle the {@code ServerHandle} owning the item
* @param itemHandle the {@code ItemHandle} to write
* @param elementDataType the {@link MxDataType} of the array's elements
* @param totalLength the total length of the expanded array
* @param elements the indices to write mapped to their scalar values; unmentioned
* indices are reset to the element type default
* @param userId the MXAccess user id used for security checks
* @throws MxGatewayException on transport or protocol failure
*/
public void writeArrayElements(
int serverHandle,
int itemHandle,
MxDataType elementDataType,
int totalLength,
Map<Integer, MxValue> elements,
int userId) {
Objects.requireNonNull(elementDataType, "elementDataType");
Objects.requireNonNull(elements, "elements");
MxSparseArray.Builder sparse = MxSparseArray.newBuilder()
.setElementDataType(elementDataType)
.setTotalLength(totalLength);
// Iterate in ascending index order so the built command is deterministic.
for (Map.Entry<Integer, MxValue> entry : new TreeMap<>(elements).entrySet()) {
sparse.addElements(MxSparseElement.newBuilder()
.setIndex(entry.getKey())
.setValue(Objects.requireNonNull(entry.getValue(), "elements value")));
}
MxValue value = MxValue.newBuilder()
.setSparseArrayValue(sparse)
.build();
writeRaw(serverHandle, itemHandle, value, userId);
}
/**
* Invokes MXAccess {@code Write2}, which carries an explicit timestamp.
*
@@ -153,6 +153,9 @@ public final class MxValues {
case TIMESTAMP_VALUE -> instant(value.getTimestampValue());
case ARRAY_VALUE -> nativeArray(value.getArrayValue());
case RAW_VALUE -> value.getRawValue().toByteArray();
// Write-only sparse descriptor: never produced by a read/decoded
// value, so it has no native representation.
case SPARSE_ARRAY_VALUE -> null;
case KIND_NOT_SET -> null;
};
}
@@ -19,6 +19,7 @@ import io.grpc.stub.ServerCallStreamObserver;
import io.grpc.stub.StreamObserver;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -36,7 +37,10 @@ import mxaccess_gateway.v1.MxaccessGateway.CloseSessionRequest;
import mxaccess_gateway.v1.MxaccessGateway.MxCommandKind;
import mxaccess_gateway.v1.MxaccessGateway.MxCommandReply;
import mxaccess_gateway.v1.MxaccessGateway.MxCommandRequest;
import mxaccess_gateway.v1.MxaccessGateway.MxDataType;
import mxaccess_gateway.v1.MxaccessGateway.MxEvent;
import mxaccess_gateway.v1.MxaccessGateway.MxSparseElement;
import mxaccess_gateway.v1.MxaccessGateway.MxValue;
import mxaccess_gateway.v1.MxaccessGateway.OpenSessionReply;
import mxaccess_gateway.v1.MxaccessGateway.OpenSessionRequest;
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatus;
@@ -396,6 +400,57 @@ final class MxGatewayClientSessionTests {
}
}
@Test
void writeArrayElementsBuildsSparseArrayWriteCommand() throws Exception {
AtomicReference<MxCommandRequest> commandRequest = new AtomicReference<>();
TestGatewayService service = new TestGatewayService() {
@Override
public void invoke(MxCommandRequest request, StreamObserver<MxCommandReply> responseObserver) {
commandRequest.set(request);
responseObserver.onNext(MxCommandReply.newBuilder()
.setSessionId(request.getSessionId())
.setKind(request.getCommand().getKind())
.setProtocolStatus(ok())
.build());
responseObserver.onCompleted();
}
};
try (InProcessGateway gateway = InProcessGateway.start(service, new AtomicReference<>());
MxGatewayClient client = gateway.client("", Duration.ofSeconds(5))) {
MxGatewaySession session = MxGatewaySession.forSessionId(client, "sparse-session");
// Supply indices out of order to prove deterministic ascending iteration.
Map<Integer, MxValue> elements = Map.of(
3, MxValues.int32Value(99),
1, MxValues.int32Value(7));
session.writeArrayElements(12, 34, MxDataType.MX_DATA_TYPE_INTEGER, 5, elements, 56);
MxCommandRequest request = commandRequest.get();
assertNotNull(request);
assertEquals(MxCommandKind.MX_COMMAND_KIND_WRITE, request.getCommand().getKind());
assertEquals(12, request.getCommand().getWrite().getServerHandle());
assertEquals(34, request.getCommand().getWrite().getItemHandle());
assertEquals(56, request.getCommand().getWrite().getUserId());
MxValue written = request.getCommand().getWrite().getValue();
assertEquals(MxValue.KindCase.SPARSE_ARRAY_VALUE, written.getKindCase());
assertEquals(5, written.getSparseArrayValue().getTotalLength());
assertEquals(
MxDataType.MX_DATA_TYPE_INTEGER,
written.getSparseArrayValue().getElementDataType());
List<MxSparseElement> sparse = written.getSparseArrayValue().getElementsList();
assertEquals(2, sparse.size());
// Ascending index order is guaranteed by the helper.
assertEquals(1, sparse.get(0).getIndex());
assertEquals(7, sparse.get(0).getValue().getInt32Value());
assertEquals(3, sparse.get(1).getIndex());
assertEquals(99, sparse.get(1).getValue().getInt32Value());
}
}
private static ProtocolStatus ok() {
return ProtocolStatus.newBuilder()
.setCode(ProtocolStatusCode.PROTOCOL_STATUS_CODE_OK)