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.