1

tl;dr: I'm looking for a way to do a more sophisticated property mapping, relying on interfaces to allow for a more generic way to assemble properties into my POJO. I don't want to have to define and check every single field, if I can help it. How can I go about doing that?


I'm attempting to create a DSL for some simplistic Camel routes to allow for non-programmers to write Camel routes. I'm not interested in a Camel-oriented solution, because the patterns are so direct that this should be accomplished from a rudimentary, simple-enough DSL.

I've written a YAML file to represent the DSL. This is fairly simple in its implementation.

route:
  configuration:
    first:
      id: one
      cron:
        expression: 0 1 2 3 4 5
      sql:
        destinations:
          - foo.sql
          - bar.sql
          - baz.sql
          - quux.sql
    second:
      id: two
      cron:
        expression: 1 2 3 4 5 6
      sql:
        destinations:
          - alpha.sql
          - beta.sql
          - gamma.sql
          - delta.sql
      file:
        destination: /full/path/to/file
    third:
      id: three
      file:
        source: /full/path/to/file
        destination: /full/path/to/file
        convert-to: json

This is trivially mapped by the following POJO and configuration property (note that I'm using Lombok to avoid the setters/getters on this).

@Getter
@Setter
@Configuration
@ConfigurationProperties(prefix = "route")
public class RouteConfigurationProcessor {

    private Map<String, RouteConfiguration> configuration;

    @PostConstruct
    public void inspectCronValues() {
        if (configuration != null) {
            for (Map.Entry<String, RouteConfiguration> cron : configuration.entrySet()) {
                System.out.println(cron);
            }
        }
    }
}


@Data
public class RouteConfiguration {
    private FileRouteConfiguration file;
    private SqlRouteConfiguration sql;
    private CronRouteConfiguration cron;
    private AwsRouteConfiguration aws;

    @Data
    static class FileRouteConfiguration {
        private String source;
        private String destination;
        private String convertTo;
    }

    @Data
    static class SqlRouteConfiguration {
        private String source;
        private List<String> destinations;
    }

    @Data
    static class CronRouteConfiguration {
        private String source;
    }

    @Data
    static class AwsRouteConfiguration {
        private String source;
        private String destination;
    }
}

Now this is all good and dandy if I want to keep those as properties, but what I want in my DSL is a bit more sophistication; I want to be able to explicitly set one of those components as sources or sinks.

So, if I modify my DSL to look the way I want...I can't get the mapping I desire because I now have no way to really tell Spring what is a Source or a Sink. Note that I've defined two essentially marker interfaces to indicate this.

 route:
  configuration:
    first:
      id: one
      source:
        cron:
          expression: 0 1 2 3 4 5
      sink:
        - sql:
          destinations:
          - foo.sql
          - bar.sql
          - baz.sql
          - quux.sql
    second:
      id: two
      source:
        cron:
          expression: 1 2 3 4 5 6
      sink:
        - sql:
          destinations:
            - alpha.sql
            - beta.sql
            - gamma.sql
            - delta.sql
        - file:
            destination: /full/path/to/file
    third:
      id: three
      source:
        file:
          source: /full/path/to/file
      sink:
        - file:
          destination: /full/path/to/file
          convert-to: json


@Data
public class RouteConfiguration {
    private String routeId;

    private Source source;

    private List<Sink> sink;

    private FileRouteConfiguration file;
    private SqlRouteConfiguration sql;
    private CronRouteConfiguration cron;
    private AwsRouteConfiguration aws;


    @Data
    static class FileRouteConfiguration implements Source, Sink {
        private String source;
        private String destination;
        private String convertTo;
    }

    @Data
    static class SqlRouteConfiguration implements Source, Sink {
        private String source;
        private List<String> scripts;
    }

    @Data
    static class CronRouteConfiguration implements Source {
        private String expression;
    }

    @Data
    static class AwsRouteConfiguration implements Source, Sink {
        private String source;
        private String destination;
    }
}

The above POJO isn't going to work simply because Spring has no way to tell what properties to set on the sink when it's provided here.

Binding to target [Bindable@79e8de45 type = java.util.List<foo.bar.Sink>, value = 'provided', annotations = array<Annotation>[[empty]]] failed:

    Property: route.configuration.first.sink[0].destinations[0]
    Value: foo.sql
    Origin: class path resource [application.yml]:20:13
    Reason: The elements [route.configuration.first.sink[0].destinations[0],route.configuration.first.sink[0].destinations[1],route.configuration.first.sink[0].destinations[2],route.configuration.first.sink[0].destinations[3],route.configuration.first.sink[0].sql] were left unbound.

How do I make this more sophisticated and take control of binding these properties myself? A Converter don't seem to register in the bean context or even touch this, and the documentation around Spring Boot 2.0+'s new property binding doesn't give or provide concrete examples.

Makoto
  • 104,088
  • 27
  • 192
  • 230
  • Given your example above. How should any framework know wich implementation of `Source` or `Sink` to use? Should it be concluded based on the properties? – dpr Mar 11 '19 at 09:22
  • @dpr: That's kind of where I want to step in anyway - if I can provide some hints as to which ones to use at which times, that'd be enough, and I can put them into the appropriate fields on my own. – Makoto Mar 11 '19 at 15:12
  • I know something like you describe can be done with JSON using the Jackson and [Polymorphic Type Handling](https://github.com/FasterXML/jackson-docs/wiki/JacksonPolymorphicDeserialization). I doubt this is supported by spring config. Not sure though. – dpr Mar 11 '19 at 15:36
  • @dpr: That's an interesting nugget. I don't mind doing that part myself if I have to - setting up a service at initialization to do this wouldn't be the worst thing. Mind putting an answer together? – Makoto Mar 11 '19 at 15:38
  • Will try to, if I have some spare time within the next 4 days ;) – dpr Mar 11 '19 at 15:39
  • Actually @dpr, hang on - I'm looking to see if there's a dupe on this. Your comment gave me enough of a clue. – Makoto Mar 11 '19 at 15:39
  • I have an answer on that one. May I ask why is the bounty gone? @Cody Gray is it related to the fact that you've marked it as duplicate? – Stelios Adamantidis Mar 12 '19 at 09:41
  • @SteliosAdamantidis: I had asked for this to be marked as a duplicate since I found the dupe to be roughly what I was looking for. If you do have something to contribute you could answer the same dupe. – Makoto Mar 12 '19 at 14:37
  • @Makoto I think that what you are asking for is not exactly the same. I will contribute when I see a bounty. Sorry. Curious though to see if you solved it with the advice given in the other answer. – Stelios Adamantidis Mar 12 '19 at 15:26

0 Answers0