feat(client-java): add writeArrayElements default-fill helper and document semantics
This commit is contained in:
@@ -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
|
||||
|
||||
+9506
-436
File diff suppressed because it is too large
Load Diff
+53
@@ -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.
|
||||
*
|
||||
|
||||
+3
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
+55
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user