3

I have a bunch of entities, I would like to load from the application.yaml file. The structure seems rather complex but it is as it is and that includes polymorphism with some classes:

This is the base structor for the properties I want to load:

@Configuration
@ConfigurationProperties(prefix = "groups")
@Data
public class GroupsServiceProperties {
    private List<GroupProperties> groups;
    private List<AggregateProperties> aggregates;
}
@Data
public class GroupProperties {
    private String id;  
    private Map<String, String> labels; 
    private SelectorProperties selector;    
}
@Data
public class AggregateProperties {
    private String id;
    private String type;
    private Map<String, String> labels;
    private SelectorProperties selector;
    
}

And this is where the interesting part is:

@Data
public class SelectorProperties {
    private String type;
}

@Data
@EqualsAndHashCode(callSuper = false)
public class LabelSelectorProperties extends SelectorProperties {
    private String name;
    private Operator operator;
    private String value;
}

@Data
@EqualsAndHashCode(callSuper = false)
public class CompositeSelectorProperties extends SelectorProperties {
    private Operator operator;
    private List<SelectorProperties> selectors;
}

Now let me give some example of YAML that I want to load:

groups:
  groups:
  - id: office
    labels:
      name: Office
    selector:
      type: LabelSelector
      name: room
      operator: EQUALS
      value: office
  - id: all_power
    labels:
      name: Power
    selector:
      type: CompositeSelector
      operator: AND
      selectors:
      - type: LabelSelector
        name: property
        operator: EQUALS
        value: power
  aggregates:
  - id: indoor_temperature
    type: QuantityAggregate
    labels:
      name: Indoor Temperature
    selector:
      type: CompositeSelector
      operator: AND
      selectors:
      - type: LabelSelector
        name: area
        operator: EQUALS
        value: indoor
      - type: LabelSelector
        name: device
        operator: EQUALS
        value: sensor
      - type: LabelSelector
        name: property
        operator: EQUALS
        value: temperature

So there are two GroupProperties in groups.groups and one AggregateProperties in groups.aggregates.

The first GroupProperties works, since it is referencing a LabelSelectorProperties directly. The LabelSelectorProperties instance has all the values.

The second GroupProperties does NOT work. What I get is an instance of CompositeSelector with a SelectorProperties instead of LabelSelectorProperties.

The same is the case for the AggregateProperties in the list.

So it looks like, my BindHandler is working unless when it comes to lists. Is that possible?

@Component
public class CustomBindHandlerAdvisor implements ConfigurationPropertiesBindHandlerAdvisor {
    
    @Override
    public BindHandler apply(BindHandler bindHandler) {
        return new CustomBindHandler(bindHandler);
    }

}

public class CustomBindHandler extends AbstractBindHandler {
    
    private static final Logger logger = LoggerFactory.getLogger(CustomBindHandler.class);
    
    private final Map<String, Class<? extends SelectorProperties>> configClasses = new HashMap<>();

    
    public CustomBindHandler(BindHandler parent) {
        super(parent);
        configClasses.put("LabelSelector", LabelSelectorProperties.class);
        configClasses.put("CompositeSelector", CompositeSelectorProperties.class);
    }

    @Override
    public Object onSuccess(ConfigurationPropertyName name, Bindable<?> target, BindContext context, Object result) {
        if (result instanceof SelectorProperties) {
            SelectorProperties config = ((SelectorProperties) result);
            if (configClasses.containsKey(config.getType())) {
                Class<? extends SelectorProperties> aClass = configClasses.get(config.getType());
                BindResult<? extends SelectorProperties> r = context.getBinder().bind(name, Bindable.of(aClass));
                return r.get();
            } else {
                logger.error("unable to bind type {}", config.getType());
            }
        }
        return super.onSuccess(name, target, context, result);
    }
}

I added a System.out to the onSuccess method of the CustomBindHandler to see which properties it is actually consulted for. It never gets asked for groups.aggregates[0].selector.selectors[0] which is what I would expect.

groups.aggregates[0].id
groups.aggregates[0].labels.name
groups.aggregates[0].labels
groups.aggregates[0].selector.type
groups.aggregates[0].selector
groups.aggregates[0].type
groups.aggregates[0]
groups.aggregates
groups.groups[0].id
groups.groups[0].labels.name
groups.groups[0].labels
groups.groups[0].selector.type
groups.groups[0].selector
groups.groups[0]
groups.groups[1].id
groups.groups[1].labels.name
groups.groups[1].labels
groups.groups[1].selector.type
groups.groups[1].selector
groups.groups[1]
groups.groups[2].id
groups.groups[2].labels.name
groups.groups[2].labels
groups.groups[2].selector.type
groups.groups[2].selector
groups.groups[2]
groups.groups[3].id
groups.groups[3].labels.name
groups.groups[3].labels
groups.groups[3].selector.type
groups.groups[3].selector
groups.groups[3]
groups.groups
groups

What do I need to do to make this work for the list in CompositeSelector?

EDIT: I created a demo to illustrate the problem here: https://github.com/mathias-ewald/spring-configuration-properties-list-of-polymorphic-types

user3235738
  • 335
  • 4
  • 22

0 Answers0