diff --git a/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java b/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java index f2d867ddb6..b2d160f5f3 100644 --- a/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java @@ -69,9 +69,11 @@ import org.thingsboard.server.service.security.auth.rest.LoginResponse; import java.lang.reflect.Field; import java.lang.reflect.Modifier; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; +import java.util.Deque; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -727,6 +729,37 @@ public class SwaggerConfiguration { return new ApiResponse().description(description).content(content); } + /** + * Recursively collects all property names reachable from {@code schemaName}, walking the + * ancestor chain through allOf $ref entries (to handle multi-level inheritance). + * {@code visited} prevents infinite loops in case of circular references. + */ + @SuppressWarnings("unchecked") + private void collectAllProperties(String schemaName, Map allSchemas, + Set result, Set visited) { + if (!visited.add(schemaName)) { + return; + } + Schema schema = allSchemas.get(schemaName); + if (schema == null) { + return; + } + if (schema.getProperties() != null) { + result.addAll(schema.getProperties().keySet()); + } + if (schema.getAllOf() != null) { + for (Schema allOfElement : schema.getAllOf()) { + String ref = allOfElement.get$ref(); + if (ref != null) { + String refName = ref.substring(ref.lastIndexOf('/') + 1); + collectAllProperties(refName, allSchemas, result, visited); + } else if (allOfElement.getProperties() != null) { + result.addAll(allOfElement.getProperties().keySet()); + } + } + } + } + @SuppressWarnings("unchecked") private void deduplicateAllOfProperties(Schema schema, Map allSchemas) { if (schema.getAllOf() == null) { @@ -744,16 +777,14 @@ public class SwaggerConfiguration { } } - // Collect properties defined in any $ref'd parent within the allOf + // Collect properties defined in any $ref'd parent within the allOf, recursively + // walking the ancestor chain (each parent may itself use allOf to extend a grandparent). Set parentProperties = new LinkedHashSet<>(); for (Schema allOfElement : schema.getAllOf()) { String ref = allOfElement.get$ref(); if (ref != null) { String refName = ref.substring(ref.lastIndexOf('/') + 1); - Schema parent = allSchemas.get(refName); - if (parent != null && parent.getProperties() != null) { - parentProperties.addAll(parent.getProperties().keySet()); - } + collectAllProperties(refName, allSchemas, parentProperties, new LinkedHashSet<>()); } } @@ -777,10 +808,9 @@ public class SwaggerConfiguration { return false; } if (allOfElement.getProperties() != null) { - allOfElement.getProperties().keySet().removeAll(toStrip); - if (allOfElement.getProperties().isEmpty()) { - allOfElement.setProperties(null); - } + Map filtered = new LinkedHashMap<>(allOfElement.getProperties()); + filtered.keySet().removeAll(toStrip); + allOfElement.setProperties(filtered.isEmpty() ? null : filtered); } return allOfElement.getProperties() == null && allOfElement.getRequired() == null @@ -789,10 +819,9 @@ public class SwaggerConfiguration { // Remove stripped properties from the schema's required list if (schema.getRequired() != null) { - schema.getRequired().removeAll(toStrip); - if (schema.getRequired().isEmpty()) { - schema.setRequired(null); - } + List required = new ArrayList<>(schema.getRequired()); + required.removeAll(toStrip); + schema.setRequired(required.isEmpty() ? null : required); } } @@ -818,19 +847,24 @@ public class SwaggerConfiguration { } private static List resolvePropertyOrder(Class cls, com.fasterxml.jackson.databind.BeanDescription beanDesc) { - // If an explicit @JsonPropertyOrder is present on the class or any directly implemented - // interface, honour it directly. Walk up the hierarchy so annotations on superclasses - // and their interfaces are also found. + // If an explicit @JsonPropertyOrder is present on the class or any interface in its + // ancestry, honour it directly. Walk up the class hierarchy; for each class also walk + // the full interface hierarchy (including super-interfaces) via BFS. for (Class c = cls; c != null && c != Object.class; c = c.getSuperclass()) { JsonPropertyOrder propOrder = c.getAnnotation(JsonPropertyOrder.class); if (propOrder != null && !propOrder.alphabetic() && propOrder.value().length > 0) { return Arrays.asList(propOrder.value()); } - for (Class iface : c.getInterfaces()) { + Deque> ifaceQueue = new ArrayDeque<>(Arrays.asList(c.getInterfaces())); + Set> visitedIfaces = new LinkedHashSet<>(); + while (!ifaceQueue.isEmpty()) { + Class iface = ifaceQueue.poll(); + if (!visitedIfaces.add(iface)) continue; propOrder = iface.getAnnotation(JsonPropertyOrder.class); if (propOrder != null && !propOrder.alphabetic() && propOrder.value().length > 0) { return Arrays.asList(propOrder.value()); } + ifaceQueue.addAll(Arrays.asList(iface.getInterfaces())); } } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java index 21497e0f89..a38e2e058e 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/DefaultEntityExportService.java @@ -70,6 +70,7 @@ public class DefaultEntityExportService ctx, I entityId) throws ThingsboardException { + @SuppressWarnings("unchecked") D exportData = (D) EntityExportData.newInstance(entityId.getEntityType()); E entity = exportableEntitiesService.findEntityByTenantIdAndId(ctx.getTenantId(), entityId);