This is an automated email from the ASF dual-hosted git repository.
chrisdutz pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/plc4x.git
The following commit(s) were added to refs/heads/develop by this push:
new f4c5ca4015 bug(plc4j/opm): OPM mishandles List and array fields
f4c5ca4015 is described below
commit f4c5ca401542e97d289318a732b72d846dc5af72
Author: Christofer Dutz <[email protected]>
AuthorDate: Fri Aug 14 16:16:41 2026 +0200
bug(plc4j/opm): OPM mishandles List and array fields
Fixes: #1947
---
.../plc4x/java/opm/PlcEntityInterceptor.java | 105 +++++++++++-
.../apache/plc4x/java/opm/CollectionFieldTest.java | 181 +++++++++++++++++++++
2 files changed, 283 insertions(+), 3 deletions(-)
diff --git
a/plc4j/tools/opm/src/main/java/org/apache/plc4x/java/opm/PlcEntityInterceptor.java
b/plc4j/tools/opm/src/main/java/org/apache/plc4x/java/opm/PlcEntityInterceptor.java
index 7b731be905..39a3358be8 100644
---
a/plc4j/tools/opm/src/main/java/org/apache/plc4x/java/opm/PlcEntityInterceptor.java
+++
b/plc4j/tools/opm/src/main/java/org/apache/plc4x/java/opm/PlcEntityInterceptor.java
@@ -31,8 +31,11 @@ import org.apache.plc4x.java.api.types.PlcResponseCode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.Instant;
@@ -40,8 +43,11 @@ import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Callable;
@@ -378,7 +384,7 @@ public class PlcEntityInterceptor {
// Fill into Cache
lastFetched.put(field.getName(), Instant.now());
- Object value = getTyped(m.getReturnType(), response, fqn);
+ Object value = getTyped(m.getReturnType(),
m.getGenericReturnType(), response, fqn);
setForField(field, proxy, value);
} catch (ClassCastException e) {
throw new OPMException("Unable to return response as suitable
type", e);
@@ -462,21 +468,114 @@ public class PlcEntityInterceptor {
Field field = clazz.getDeclaredField(targetTagName);
field.setAccessible(true);
try {
- field.set(o, getTyped(field.getType(), response, sourceTagName));
+ field.set(o, getTyped(field.getType(), field.getGenericType(),
response, sourceTagName));
} catch (ClassCastException e) {
throw new PlcRuntimeException(String.format("Unable to assign
return value %s to tag %s with type %s",
response.getObject(sourceTagName), targetTagName,
field.getType()), e);
}
}
+ /** The element type of a collection type, or Object when it isn't known.
*/
+ private static Class<?> elementType(Type genericType) {
+ if (genericType instanceof ParameterizedType parameterized) {
+ Type[] arguments = parameterized.getActualTypeArguments();
+ if (arguments.length == 1 && arguments[0] instanceof Class<?>
elementClass) {
+ return elementClass;
+ }
+ }
+ return Object.class;
+ }
+
+ /** Reads all values of a tag and returns them as an array of the given
component type. */
+ private static Object toArray(Class<?> componentType, PlcReadResponse
response, String sourceFieldName) {
+ List<?> values = new ArrayList<>(getAllTyped(componentType, response,
sourceFieldName));
+ Object array = Array.newInstance(componentType, values.size());
+ for (int i = 0; i < values.size(); i++) {
+ // Array.set unboxes for primitive component types.
+ Array.set(array, i, values.get(i));
+ }
+ return array;
+ }
+
+ /** Reads all values of a tag into a collection assignable to the declared
collection type. */
+ private static Object toCollection(Class<?> clazz, Class<?> elementType,
PlcReadResponse response,
+ String sourceFieldName) {
+ Collection<?> values = getAllTyped(elementType, response,
sourceFieldName);
+ if (clazz.isAssignableFrom(ArrayList.class)) {
+ return new ArrayList<>(values);
+ }
+ if (clazz.isAssignableFrom(LinkedHashSet.class)) {
+ return new LinkedHashSet<>(values);
+ }
+ throw new ClassCastException("Unable to return the values of tag " +
sourceFieldName
+ + " as an instance of " + clazz + ". Use List, Collection, Set or
an array.");
+ }
+
+ /**
+ * The typed accessor matching the element type, mirroring the scalar
conversions below.
+ * Unknown types fall back to the plain objects of the response.
+ */
+ private static Collection<?> getAllTyped(Class<?> elementType,
PlcReadResponse response, String sourceFieldName) {
+ if (elementType == boolean.class || elementType == Boolean.class) {
+ return response.getAllBooleans(sourceFieldName);
+ } else if (elementType == byte.class || elementType == Byte.class) {
+ return response.getAllBytes(sourceFieldName);
+ } else if (elementType == short.class || elementType == Short.class) {
+ return response.getAllShorts(sourceFieldName);
+ } else if (elementType == int.class || elementType == Integer.class) {
+ return response.getAllIntegers(sourceFieldName);
+ } else if (elementType == long.class || elementType == Long.class) {
+ return response.getAllLongs(sourceFieldName);
+ } else if (elementType == float.class || elementType == Float.class) {
+ return response.getAllFloats(sourceFieldName);
+ } else if (elementType == double.class || elementType == Double.class)
{
+ return response.getAllDoubles(sourceFieldName);
+ } else if (elementType == BigInteger.class) {
+ return response.getAllBigIntegers(sourceFieldName);
+ } else if (elementType == BigDecimal.class) {
+ return response.getAllBigDecimals(sourceFieldName);
+ } else if (elementType == String.class) {
+ return response.getAllStrings(sourceFieldName);
+ } else if (elementType == LocalTime.class) {
+ return response.getAllTimes(sourceFieldName);
+ } else if (elementType == LocalDate.class) {
+ return response.getAllDates(sourceFieldName);
+ } else if (elementType == LocalDateTime.class) {
+ return response.getAllDateTimes(sourceFieldName);
+ }
+ return response.getAllObjects(sourceFieldName);
+ }
+
+ static Object getTyped(Class<?> clazz, PlcReadResponse response, String
sourceFieldName) {
+ return getTyped(clazz, null, response, sourceFieldName);
+ }
+
+ /**
+ * Converts the value of one tag of a response into the type of the
annotated field.
+ *
+ * @param clazz the declared type of the field or the return type of
the getter
+ * @param genericType the generic type of the same, when known. It carries
the element type of a
+ * collection field ({@code List<Integer>}), which the
raw class does not -
+ * see GH-1947. May be null, in which case collection
elements come back as
+ * plain objects.
+ */
@SuppressWarnings({"squid:S3776", "squid:MethodCyclomaticComplexity"})
// Cognitive Complexity not too high, as highly structured
- static Object getTyped(Class<?> clazz, PlcReadResponse response, String
sourceFieldName) {
+ static Object getTyped(Class<?> clazz, Type genericType, PlcReadResponse
response, String sourceFieldName) {
LOGGER.debug("getTyped clazz: {}, response: {}, tagName: {}", clazz,
response, sourceFieldName);
if (response.getResponseCode(sourceFieldName) != PlcResponseCode.OK) {
throw new PlcRuntimeException(String.format("Unable to read
specified tag '%s', response code was '%s'",
sourceFieldName, response.getResponseCode(sourceFieldName)));
}
+ // A tag reading several values (e.g. "input-register:1:UINT[2]") has
to be assigned to an
+ // array or collection field, with its elements converted to the
field's element type -
+ // handing out the raw PlcValues makes every element access throw a
ClassCastException.
+ if (clazz.isArray()) {
+ return toArray(clazz.getComponentType(), response,
sourceFieldName);
+ }
+ if (Collection.class.isAssignableFrom(clazz)) {
+ return toCollection(clazz, elementType(genericType), response,
sourceFieldName);
+ }
if (clazz.isPrimitive()) {
if (clazz == boolean.class) {
return response.getBoolean(sourceFieldName);
diff --git
a/plc4j/tools/opm/src/test/java/org/apache/plc4x/java/opm/CollectionFieldTest.java
b/plc4j/tools/opm/src/test/java/org/apache/plc4x/java/opm/CollectionFieldTest.java
new file mode 100644
index 0000000000..de872d90ba
--- /dev/null
+++
b/plc4j/tools/opm/src/test/java/org/apache/plc4x/java/opm/CollectionFieldTest.java
@@ -0,0 +1,181 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.plc4x.java.opm;
+
+import org.apache.plc4x.java.api.types.PlcResponseCode;
+import org.apache.plc4x.java.spi.drivers.messages.DefaultPlcReadResponse;
+import org.apache.plc4x.java.spi.drivers.messages.items.DefaultPlcResponseItem;
+import org.apache.plc4x.java.spi.drivers.messages.items.PlcResponseItem;
+import org.apache.plc4x.java.api.value.PlcValue;
+import org.apache.plc4x.java.spi.values.PlcList;
+import org.apache.plc4x.java.spi.values.PlcUINT;
+import org.apache.plc4x.java.DefaultPlcDriverManager;
+import org.apache.plc4x.java.mock.connection.MockConnection;
+import org.apache.plc4x.java.mock.connection.MockDevice;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import java.lang.reflect.Field;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * A tag reading several values has to be converted to the element type of the
annotated field -
+ * see GH-1947. The values used to be handed out as the raw PlcValues, so
+ * {@code List<Integer> l; l.get(0)} threw a ClassCastException, and an array
field could not be
+ * assigned at all.
+ */
+class CollectionFieldTest {
+
+ /** The reporter's field: {@code @PlcTag("input-register:1:UINT[2]")
List<Integer> registers;} */
+ private List<Integer> registers;
+ private Collection<Integer> asCollection;
+ private Set<Integer> asSet;
+ private int[] asPrimitiveArray;
+ private Integer[] asBoxedArray;
+ private List<String> asStrings;
+ private List<?> withoutElementType;
+ private java.util.Deque<Integer> unsupportedCollection;
+
+ @Test
+ void listOfIntegerYieldsIntegers() throws Exception {
+ Object value = convert("registers");
+
+ assertInstanceOf(List.class, value);
+ List<?> list = (List<?>) value;
+ assertEquals(2, list.size());
+ // The point of the issue: the elements are Integers, not PlcUINTs.
+ assertEquals(Integer.valueOf(1), list.get(0));
+ assertEquals(Integer.valueOf(2), list.get(1));
+ for (Object element : list) {
+ assertInstanceOf(Integer.class, element);
+ }
+ }
+
+ @Test
+ void collectionAndSetAreAlsoSupported() throws Exception {
+ assertInstanceOf(Collection.class, convert("asCollection"));
+ assertInstanceOf(Set.class, convert("asSet"));
+ assertEquals(2, ((Collection<?>) convert("asCollection")).size());
+ }
+
+ /**
+ * The reporter's second finding: an array field could not be assigned at
all.
+ */
+ @Test
+ void primitiveArrayIsAssignable() throws Exception {
+ Object value = convert("asPrimitiveArray");
+
+ assertInstanceOf(int[].class, value);
+ assertArrayEquals(new int[]{1, 2}, (int[]) value);
+ }
+
+ @Test
+ void boxedArrayIsAssignable() throws Exception {
+ Object value = convert("asBoxedArray");
+
+ assertInstanceOf(Integer[].class, value);
+ assertArrayEquals(new Integer[]{1, 2}, (Integer[]) value);
+ }
+
+ @Test
+ void elementTypeDrivesTheConversion() throws Exception {
+ Object value = convert("asStrings");
+
+ List<?> list = (List<?>) value;
+ assertInstanceOf(String.class, list.get(0));
+ }
+
+ /**
+ * Without a resolvable element type the values come back as plain objects
rather than failing.
+ */
+ @Test
+ void unknownElementTypeFallsBackToObjects() throws Exception {
+ Object value = convert("withoutElementType");
+
+ assertEquals(2, ((List<?>) value).size());
+ }
+
+ @Test
+ void unsupportedCollectionTypeIsReported() {
+ assertThrows(ClassCastException.class, () ->
convert("unsupportedCollection"));
+ }
+
+ /**
+ * The whole way through OPM, as the reporter used it: a @PlcTag on a List
field, read through
+ * the entity manager.
+ */
+ @Test
+ void entityWithAListFieldIsPopulated() throws Exception {
+ DefaultPlcDriverManager driverManager = new DefaultPlcDriverManager();
+ MockConnection connection = (MockConnection)
driverManager.getConnection("mock:collections");
+ MockDevice mockDevice = Mockito.mock(MockDevice.class);
+ Mockito.when(mockDevice.read(Mockito.any())).thenReturn(
+ new DefaultPlcResponseItem<>(PlcResponseCode.OK,
+ new PlcList(List.of(new PlcUINT(1), new PlcUINT(2)))));
+ connection.setDevice(mockDevice);
+
+ RegisterEntity entity = new PlcEntityManager(driverManager)
+ .read(RegisterEntity.class, "mock:collections");
+
+ assertEquals(List.of(1, 2), entity.getRegisters());
+ assertArrayEquals(new int[]{1, 2}, entity.getRaw());
+ }
+
+ @PlcEntity
+ public static class RegisterEntity {
+
+ @PlcTag("input-register:1:UINT[2]")
+ private List<Integer> registers;
+
+ @PlcTag("input-register:1:UINT[2]")
+ private int[] raw;
+
+ public RegisterEntity() {
+ // For OPM
+ }
+
+ public List<Integer> getRegisters() {
+ return registers;
+ }
+
+ public int[] getRaw() {
+ return raw;
+ }
+ }
+
+ /** Converts a two-element UINT response into the declared type of the
named field. */
+ private Object convert(String fieldName) throws NoSuchFieldException {
+ Field field = CollectionFieldTest.class.getDeclaredField(fieldName);
+ return PlcEntityInterceptor.getTyped(field.getType(),
field.getGenericType(), response(), "tag");
+ }
+
+ private DefaultPlcReadResponse response() {
+ PlcValue value = new PlcList(List.of(new PlcUINT(1), new PlcUINT(2)));
+ PlcResponseItem<PlcValue> item = new
DefaultPlcResponseItem<>(PlcResponseCode.OK, value);
+ return new DefaultPlcReadResponse(null,
Collections.singletonMap("tag", item));
+ }
+}