Browse Source

fixed openapi schema ordering

pull/15239/head
dashevchenko 7 months ago
parent
commit
c3b0ce2ca2
  1. 136
      application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java
  2. 2
      application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java
  3. 2
      common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java

136
application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java

@ -83,6 +83,7 @@ import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
@ -110,6 +111,11 @@ public class SwaggerConfiguration {
private static final ApiResponses defaultErrorResponses = defaultErrorResponses(false); private static final ApiResponses defaultErrorResponses = defaultErrorResponses(false);
private static final ApiResponses defaultPostErrorResponses = defaultErrorResponses(true); private static final ApiResponses defaultPostErrorResponses = defaultErrorResponses(true);
// Populated by mapAwareConverter, consumed by customOpenApiCustomizer.
// Keyed by Class.getSimpleName() which matches swagger-core's default schema naming.
private final Map<String, List<String>> schemaPropertyOrders = new ConcurrentHashMap<>();
private final Map<String, Set<String>> schemaOwnProps = new ConcurrentHashMap<>();
@Value("${swagger.api_path:/api/**}") @Value("${swagger.api_path:/api/**}")
private String apiPath; private String apiPath;
@Value("${swagger.security_path_regex}") @Value("${swagger.security_path_regex}")
@ -312,62 +318,20 @@ public class SwaggerConfiguration {
schema.setProperties(null); schema.setProperties(null);
} }
} }
} else if (schema != null) { } else {
boolean hasAllOf = schema.getAllOf() != null; // Precompute property order and own-prop names for this class.
boolean hasProps = schema.getProperties() != null && !schema.getProperties().isEmpty(); // The actual reordering happens later in the OpenApiCustomizer,
if (hasAllOf || hasProps) { // which has access to the final state of all component schemas
try { // (including ones where the ModelConverter only sees a $ref).
var beanDesc = Json.mapper().getSerializationConfig().introspect(javaType); try {
var orderedNames = resolvePropertyOrder(cls, beanDesc); var beanDesc = Json.mapper().getSerializationConfig().introspect(javaType);
// Reorder top-level properties if present. schemaPropertyOrders.put(cls.getSimpleName(), resolvePropertyOrder(cls, beanDesc));
// When orderedNames is empty (e.g. for interfaces where Jackson Set<String> ownProps = computeOwnPropNames(cls, beanDesc);
// returns no properties from beanDesc), fall through to the if (!ownProps.isEmpty()) {
// TreeMap fallback which sorts remaining properties alphabetically. schemaOwnProps.put(cls.getSimpleName(), ownProps);
if (hasProps) {
@SuppressWarnings("unchecked")
Map<String, Schema> current = schema.getProperties();
var reordered = new LinkedHashMap<String, Schema>();
for (String name : orderedNames) {
Schema prop = current.get(name);
if (prop != null) reordered.put(name, prop);
}
// Any properties not covered by orderedNames are appended
// alphabetically to guarantee a deterministic stable order.
new TreeMap<>(current).forEach((k, v) -> reordered.putIfAbsent(k, v));
schema.setProperties(reordered);
}
// Also reorder properties inside allOf inline elements, and mark
// which properties are declared in cls itself (not inherited from
// a superclass) so deduplicateAllOfProperties can strip inherited
// ones without touching own-class properties.
if (hasAllOf) {
Set<String> ownProps = computeOwnPropNames(cls, beanDesc);
if (!ownProps.isEmpty()) {
schema.addExtension("x-tb-own-props", List.copyOf(ownProps));
}
@SuppressWarnings("unchecked")
List<Schema> allOfList = schema.getAllOf();
for (Schema allOfElement : allOfList) {
if (allOfElement.get$ref() != null) continue;
@SuppressWarnings("unchecked")
Map<String, Schema> inlineProps = allOfElement.getProperties();
if (inlineProps == null || inlineProps.isEmpty()) continue;
var reordered = new LinkedHashMap<String, Schema>();
for (String name : orderedNames) {
Schema prop = inlineProps.get(name);
if (prop != null) reordered.put(name, prop);
}
new TreeMap<>(inlineProps).forEach((k, v) -> reordered.putIfAbsent(k, v));
allOfElement.setProperties(reordered);
}
}
} catch (Exception e) {
log.debug("Failed to resolve property order for {}: {}", cls.getName(), e.getMessage());
// Fallback: at minimum sort alphabetically for determinism
if (hasProps) {
schema.setProperties(new LinkedHashMap<>(new TreeMap<>(schema.getProperties())));
}
} }
} catch (Exception e) {
log.debug("Failed to resolve property order for {}: {}", cls.getName(), e.getMessage());
} }
} }
} }
@ -454,16 +418,18 @@ public class SwaggerConfiguration {
}); });
// Deduplicate allOf child schemas: remove properties that are already defined // Deduplicate allOf child schemas: remove properties that are already defined
// in the referenced parent schema to avoid duplication (e.g. EntityId children), // in the referenced parent schema to avoid duplication (e.g. EntityId children).
// then clean up the internal marker extension used during deduplication. schemas.forEach((schemaName, schema) -> {
schemas.values().forEach(schema -> { Set<String> ownProps = schemaOwnProps.getOrDefault(schemaName, Set.of());
deduplicateAllOfProperties(schema, schemas); deduplicateAllOfProperties(schema, schemas, ownProps);
if (schema.getExtensions() != null) { });
schema.getExtensions().remove("x-tb-own-props");
if (schema.getExtensions().isEmpty()) { // Reorder properties for all component schemas. This runs after all
schema.setExtensions(null); // schemas are finalized so it covers schemas the ModelConverter only
} // saw as a $ref (e.g. interface-based discriminator types like EntityId).
} schemas.forEach((schemaName, schema) -> {
List<String> propOrder = schemaPropertyOrders.getOrDefault(schemaName, List.of());
reorderSchemaProperties(schema, propOrder);
}); });
// Fix polymorphic request/response bodies: replace inline oneOf with base type $ref // Fix polymorphic request/response bodies: replace inline oneOf with base type $ref
@ -771,22 +737,11 @@ public class SwaggerConfiguration {
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private void deduplicateAllOfProperties(Schema<?> schema, Map<String, Schema> allSchemas) { private void deduplicateAllOfProperties(Schema<?> schema, Map<String, Schema> allSchemas, Set<String> ownProps) {
if (schema.getAllOf() == null) { if (schema.getAllOf() == null) {
return; return;
} }
// Properties declared in the class's own fields (not inherited from a superclass).
// These must NOT be stripped from the inline even if they also appear in a parent
// schema (e.g. a field that also has a corresponding interface getter in the parent).
Set<String> ownProps = new LinkedHashSet<>();
if (schema.getExtensions() != null
&& schema.getExtensions().get("x-tb-own-props") instanceof List<?> list) {
for (Object v : list) {
if (v instanceof String s) ownProps.add(s);
}
}
// Collect properties defined in any $ref'd parent within the allOf, recursively // 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). // walking the ancestor chain (each parent may itself use allOf to extend a grandparent).
Set<String> parentProperties = new LinkedHashSet<>(); Set<String> parentProperties = new LinkedHashSet<>();
@ -856,6 +811,33 @@ public class SwaggerConfiguration {
return own; return own;
} }
@SuppressWarnings("unchecked")
private static void reorderSchemaProperties(Schema<?> schema, List<String> propOrder) {
if (schema.getProperties() != null && schema.getProperties().size() > 1) {
schema.setProperties(reorderProperties(schema.getProperties(), propOrder));
}
if (schema.getAllOf() != null) {
for (Schema<?> allOfElement : schema.getAllOf()) {
if (allOfElement.get$ref() != null) continue;
if (allOfElement.getProperties() != null && allOfElement.getProperties().size() > 1) {
allOfElement.setProperties(reorderProperties(allOfElement.getProperties(), propOrder));
}
}
}
}
private static LinkedHashMap<String, Schema> reorderProperties(Map<String, Schema> current, List<String> propOrder) {
var reordered = new LinkedHashMap<String, Schema>();
for (String name : propOrder) {
Schema prop = current.get(name);
if (prop != null) reordered.put(name, prop);
}
// Any properties not covered by propOrder are appended
// alphabetically to guarantee a deterministic stable order.
new TreeMap<>(current).forEach((k, v) -> reordered.putIfAbsent(k, v));
return reordered;
}
/** /**
* Resolves the property ordering for a schema class. * Resolves the property ordering for a schema class.
* *

2
application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.controller; package org.thingsboard.server.controller;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
@ -166,6 +167,7 @@ public class TwoFactorAuthController extends BaseController {
@Builder @Builder
public static class TwoFaProviderInfo { public static class TwoFaProviderInfo {
private TwoFaProviderType type; private TwoFaProviderType type;
@JsonProperty("default")
private boolean isDefault; private boolean isDefault;
private String contact; private String contact;
private Integer minVerificationCodeSendPeriod; private Integer minVerificationCodeSendPeriod;

2
common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java

@ -33,7 +33,7 @@ import java.util.List;
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@JsonPropertyOrder({"fqn", "name", "deprecated", "image", "description", "descriptor", "externalId", "resources"}) @JsonPropertyOrder({"id", "createdTime", "tenantId", "fqn", "name", "deprecated", "scada", "version", "descriptor", "image", "description", "tags", "externalId", "resources"})
public class WidgetTypeDetails extends WidgetType implements HasName, HasTenantId, HasImage, ExportableEntity<WidgetTypeId> { public class WidgetTypeDetails extends WidgetType implements HasName, HasTenantId, HasImage, ExportableEntity<WidgetTypeId> {
@Schema(description = "Relative or external image URL. Replaced with image data URL (Base64) in case of relative URL and 'inlineImages' option enabled.") @Schema(description = "Relative or external image URL. Replaced with image data URL (Base64) in case of relative URL and 'inlineImages' option enabled.")

Loading…
Cancel
Save