Skip to content

Commit 83e81e1

Browse files
committed
Add support of metadata generation for Endpoints
This commit improves the configuration metadata annotation processor to explicitly handle `@Endpoint` annotated class. Adding a new endpoint on a project potentially creates the following keys: * `endpoints.<id>.enabled` * `endpoints.<id>.cache.time-to-live` * `endpoints.<id>.jmx.enabled` * `endpoints.<id>.web.enabled` Default values are extracted from the annotation type. If an endpoint is restricted to a given tech, properties from unrelated techs are not generated. Closes spring-projectsgh-9692
1 parent 25d2d55 commit 83e81e1

File tree

13 files changed

+558
-0
lines changed

13 files changed

+558
-0
lines changed

spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@
1919
import java.io.FileNotFoundException;
2020
import java.io.PrintWriter;
2121
import java.io.StringWriter;
22+
import java.util.ArrayList;
23+
import java.util.Arrays;
2224
import java.util.LinkedHashMap;
25+
import java.util.List;
2326
import java.util.Map;
2427
import java.util.Set;
2528

@@ -70,6 +73,9 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
7073
static final String DEPRECATED_CONFIGURATION_PROPERTY_ANNOTATION = "org.springframework.boot."
7174
+ "context.properties.DeprecatedConfigurationProperty";
7275

76+
static final String ENDPOINT_ANNOTATION = "org.springframework.boot."
77+
+ "endpoint.Endpoint";
78+
7379
static final String LOMBOK_DATA_ANNOTATION = "lombok.Data";
7480

7581
static final String LOMBOK_GETTER_ANNOTATION = "lombok.Getter";
@@ -98,6 +104,10 @@ protected String deprecatedConfigurationPropertyAnnotation() {
98104
return DEPRECATED_CONFIGURATION_PROPERTY_ANNOTATION;
99105
}
100106

107+
protected String endpointAnnotation() {
108+
return ENDPOINT_ANNOTATION;
109+
}
110+
101111
@Override
102112
public SourceVersion getSupportedSourceVersion() {
103113
return SourceVersion.latestSupported();
@@ -132,6 +142,13 @@ public boolean process(Set<? extends TypeElement> annotations,
132142
processElement(element);
133143
}
134144
}
145+
TypeElement endpointType = elementUtils.getTypeElement(endpointAnnotation());
146+
if (endpointType != null) { // Is @Endpoint available
147+
for (Element element : roundEnv.getElementsAnnotatedWith(endpointType)) {
148+
processEndpoint(element);
149+
}
150+
}
151+
135152
if (roundEnv.processingOver()) {
136153
try {
137154
writeMetaData();
@@ -332,6 +349,59 @@ private void processNestedType(String prefix, TypeElement element,
332349
}
333350
}
334351

352+
private void processEndpoint(Element element) {
353+
try {
354+
AnnotationMirror annotation = getAnnotation(element, endpointAnnotation());
355+
if (element instanceof TypeElement) {
356+
processEndpoint(annotation, (TypeElement) element);
357+
}
358+
}
359+
catch (Exception ex) {
360+
throw new IllegalStateException(
361+
"Error processing configuration meta-data on " + element, ex);
362+
}
363+
}
364+
365+
private void processEndpoint(AnnotationMirror annotation, TypeElement element) {
366+
Map<String, Object> elementValues = getAnnotationElementValues(annotation);
367+
String endpointId = (String) elementValues.get("id");
368+
if (endpointId == null || "".equals(endpointId)) {
369+
return; // Can't process that endpoint
370+
}
371+
Boolean enabledByDefault = (Boolean) elementValues.get("enabledByDefault");
372+
if (enabledByDefault == null) {
373+
enabledByDefault = Boolean.TRUE;
374+
}
375+
String type = this.typeUtils.getQualifiedName(element);
376+
this.metadataCollector.add(ItemMetadata.newGroup(endpointKey(endpointId),
377+
type, type, null));
378+
this.metadataCollector.add(ItemMetadata.newProperty(endpointKey(endpointId),
379+
"enabled", Boolean.class.getName(), type, null, String.format(
380+
"Enable the %s endpoint.", endpointId), enabledByDefault, null));
381+
this.metadataCollector.add(ItemMetadata.newProperty(endpointKey(endpointId),
382+
"cache.time-to-live", Long.class.getName(), type, null,
383+
"Maximum time in milliseconds that a response can be cached.", 0, null));
384+
385+
EndpointTypes endpointTypes = EndpointTypes.parse(elementValues.get("types"));
386+
if (endpointTypes.hasJmx()) {
387+
this.metadataCollector.add(ItemMetadata.newProperty(
388+
endpointKey(endpointId + ".jmx"), "enabled", Boolean.class.getName(),
389+
type, null, String.format("Expose the %s endpoint as a JMX MBean.",
390+
endpointId), enabledByDefault, null));
391+
}
392+
if (endpointTypes.hasWeb()) {
393+
this.metadataCollector.add(ItemMetadata.newProperty(
394+
endpointKey(endpointId + ".web"), "enabled", Boolean.class.getName(),
395+
type, null, String.format("Expose the %s endpoint as a Web endpoint.",
396+
endpointId), enabledByDefault, null));
397+
}
398+
}
399+
400+
private String endpointKey(String suffix) {
401+
return "endpoints." + suffix;
402+
}
403+
404+
335405
private boolean isNested(Element returnType, VariableElement field,
336406
TypeElement element) {
337407
if (hasAnnotation(field, nestedConfigurationPropertyAnnotation())) {
@@ -454,4 +524,41 @@ private void log(Kind kind, String msg) {
454524
this.processingEnv.getMessager().printMessage(kind, msg);
455525
}
456526

527+
private static class EndpointTypes {
528+
529+
private static final List<String> ALL_TYPES = Arrays.asList("JMX", "WEB");
530+
531+
private final List<String> types;
532+
533+
EndpointTypes(List<String> types) {
534+
this.types = types;
535+
}
536+
537+
static EndpointTypes parse(Object typesAttribute) {
538+
if (typesAttribute == null || !(typesAttribute instanceof List)) {
539+
return new EndpointTypes(ALL_TYPES);
540+
}
541+
List<AnnotationValue> values = (List<AnnotationValue>) typesAttribute;
542+
if (values.isEmpty()) {
543+
return new EndpointTypes(ALL_TYPES);
544+
}
545+
List<String> types = new ArrayList<>();
546+
for (AnnotationValue value : values) {
547+
types.add(((VariableElement) value.getValue()).getSimpleName().toString());
548+
}
549+
return new EndpointTypes(types);
550+
551+
}
552+
553+
public boolean hasJmx() {
554+
return this.types.contains("JMX");
555+
}
556+
557+
558+
public boolean hasWeb() {
559+
return this.types.contains("WEB");
560+
}
561+
562+
}
563+
457564
}

spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ConfigurationMetadata.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,4 +209,14 @@ private static <T extends Comparable<T>> List<T> flattenValues(Map<?, List<T>> m
209209
return content;
210210
}
211211

212+
@Override
213+
public String toString() {
214+
StringBuilder sb = new StringBuilder();
215+
sb.append(String.format("items: %n"));
216+
this.items.values().forEach(itemMetadata -> {
217+
sb.append("\t").append(String.format("%s%n", itemMetadata));
218+
});
219+
return sb.toString();
220+
}
221+
212222
}

spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@
3636
import org.springframework.boot.configurationprocessor.metadata.ItemMetadata;
3737
import org.springframework.boot.configurationprocessor.metadata.Metadata;
3838
import org.springframework.boot.configurationprocessor.metadata.TestJsonConverter;
39+
import org.springframework.boot.configurationsample.endpoint.CustomPropertiesEndpoint;
40+
import org.springframework.boot.configurationsample.endpoint.DisabledEndpoint;
41+
import org.springframework.boot.configurationsample.endpoint.OnlyJmxEndpoint;
42+
import org.springframework.boot.configurationsample.endpoint.OnlyWebEndpoint;
43+
import org.springframework.boot.configurationsample.endpoint.SimpleEndpoint;
44+
import org.springframework.boot.configurationsample.endpoint.incremental.IncrementalEndpoint;
45+
import org.springframework.boot.configurationsample.endpoint.incremental.IncrementalJmxEndpoint;
3946
import org.springframework.boot.configurationsample.incremental.BarProperties;
4047
import org.springframework.boot.configurationsample.incremental.FooProperties;
4148
import org.springframework.boot.configurationsample.incremental.RenamedBarProperties;
@@ -517,6 +524,164 @@ public void lombokInnerClassWithGetterProperties() throws IOException {
517524
assertThat(metadata.getItems()).hasSize(3);
518525
}
519526

527+
528+
@Test
529+
public void simpleEndpoint() throws IOException {
530+
ConfigurationMetadata metadata = compile(SimpleEndpoint.class);
531+
assertThat(metadata).has(Metadata.withGroup("endpoints.simple")
532+
.fromSource(SimpleEndpoint.class));
533+
assertThat(metadata).has(enabledFlag("simple", true));
534+
assertThat(metadata).has(jmxEnabledFlag("simple", true));
535+
assertThat(metadata).has(webEnabledFlag("simple", true));
536+
assertThat(metadata).has(cacheTtl("simple"));
537+
assertThat(metadata.getItems()).hasSize(5);
538+
}
539+
540+
@Test
541+
public void disableEndpoint() throws IOException {
542+
ConfigurationMetadata metadata = compile(DisabledEndpoint.class);
543+
assertThat(metadata).has(Metadata.withGroup("endpoints.disabled")
544+
.fromSource(DisabledEndpoint.class));
545+
assertThat(metadata).has(enabledFlag("disabled", false));
546+
assertThat(metadata).has(jmxEnabledFlag("disabled", false));
547+
assertThat(metadata).has(webEnabledFlag("disabled", false));
548+
assertThat(metadata).has(cacheTtl("disabled"));
549+
assertThat(metadata.getItems()).hasSize(5);
550+
}
551+
552+
@Test
553+
public void customPropertiesEndpoint() throws IOException {
554+
ConfigurationMetadata metadata = compile(CustomPropertiesEndpoint.class);
555+
assertThat(metadata).has(Metadata.withGroup("endpoints.customprops")
556+
.fromSource(CustomPropertiesEndpoint.class));
557+
assertThat(metadata).has(Metadata.withProperty("endpoints.customprops.name").
558+
ofType(String.class).withDefaultValue("test"));
559+
assertThat(metadata).has(enabledFlag("customprops", true));
560+
assertThat(metadata).has(jmxEnabledFlag("customprops", true));
561+
assertThat(metadata).has(webEnabledFlag("customprops", true));
562+
assertThat(metadata).has(cacheTtl("customprops"));
563+
assertThat(metadata.getItems()).hasSize(6);
564+
}
565+
566+
@Test
567+
public void jmxOnlyEndpoint() throws IOException {
568+
ConfigurationMetadata metadata = compile(OnlyJmxEndpoint.class);
569+
assertThat(metadata).has(Metadata.withGroup("endpoints.jmx")
570+
.fromSource(OnlyJmxEndpoint.class));
571+
assertThat(metadata).has(enabledFlag("jmx", true));
572+
assertThat(metadata).has(jmxEnabledFlag("jmx", true));
573+
assertThat(metadata).has(cacheTtl("jmx"));
574+
assertThat(metadata.getItems()).hasSize(4);
575+
}
576+
577+
@Test
578+
public void webOnlyEndpoint() throws IOException {
579+
ConfigurationMetadata metadata = compile(OnlyWebEndpoint.class);
580+
assertThat(metadata).has(Metadata.withGroup("endpoints.web")
581+
.fromSource(OnlyWebEndpoint.class));
582+
assertThat(metadata).has(enabledFlag("web", true));
583+
assertThat(metadata).has(webEnabledFlag("web", true));
584+
assertThat(metadata).has(cacheTtl("web"));
585+
assertThat(metadata.getItems()).hasSize(4);
586+
}
587+
588+
@Test
589+
public void incrementalEndpointBuildChangeGeneralEnabledFlag() throws Exception {
590+
TestProject project = new TestProject(this.temporaryFolder,
591+
IncrementalEndpoint.class);
592+
ConfigurationMetadata metadata = project.fullBuild();
593+
assertThat(metadata).has(Metadata.withGroup("endpoints.incremental")
594+
.fromSource(IncrementalEndpoint.class));
595+
assertThat(metadata).has(enabledFlag("incremental", true));
596+
assertThat(metadata).has(jmxEnabledFlag("incremental", true));
597+
assertThat(metadata).has(webEnabledFlag("incremental", true));
598+
assertThat(metadata).has(cacheTtl("incremental"));
599+
assertThat(metadata.getItems()).hasSize(5);
600+
project.replaceText(IncrementalEndpoint.class, "id = \"incremental\"",
601+
"id = \"incremental\", enabledByDefault = false");
602+
metadata = project.incrementalBuild(IncrementalEndpoint.class);
603+
assertThat(metadata).has(Metadata.withGroup("endpoints.incremental")
604+
.fromSource(IncrementalEndpoint.class));
605+
assertThat(metadata).has(enabledFlag("incremental", false));
606+
assertThat(metadata).has(jmxEnabledFlag("incremental", false));
607+
assertThat(metadata).has(webEnabledFlag("incremental", false));
608+
assertThat(metadata).has(cacheTtl("incremental"));
609+
assertThat(metadata.getItems()).hasSize(5);
610+
}
611+
612+
@Test
613+
public void incrementalEndpointBuildDisableJmxEndpoint() throws Exception {
614+
TestProject project = new TestProject(this.temporaryFolder,
615+
IncrementalEndpoint.class);
616+
ConfigurationMetadata metadata = project.fullBuild();
617+
assertThat(metadata).has(Metadata.withGroup("endpoints.incremental")
618+
.fromSource(IncrementalEndpoint.class));
619+
assertThat(metadata).has(enabledFlag("incremental", true));
620+
assertThat(metadata).has(jmxEnabledFlag("incremental", true));
621+
assertThat(metadata).has(webEnabledFlag("incremental", true));
622+
assertThat(metadata).has(cacheTtl("incremental"));
623+
assertThat(metadata.getItems()).hasSize(5);
624+
project.replaceText(IncrementalEndpoint.class, "id = \"incremental\"",
625+
"id = \"incremental\", types = Endpoint.Type.WEB");
626+
metadata = project.incrementalBuild(IncrementalEndpoint.class);
627+
assertThat(metadata).has(Metadata.withGroup("endpoints.incremental")
628+
.fromSource(IncrementalEndpoint.class));
629+
assertThat(metadata).has(enabledFlag("incremental", true));
630+
assertThat(metadata).has(webEnabledFlag("incremental", true));
631+
assertThat(metadata).has(cacheTtl("incremental"));
632+
assertThat(metadata.getItems()).hasSize(4);
633+
}
634+
635+
@Test
636+
public void incrementalEndpointBuildEnableJmxEndpoint() throws Exception {
637+
TestProject project = new TestProject(this.temporaryFolder,
638+
IncrementalJmxEndpoint.class);
639+
ConfigurationMetadata metadata = project.fullBuild();
640+
assertThat(metadata).has(Metadata.withGroup("endpoints.incremental")
641+
.fromSource(IncrementalJmxEndpoint.class));
642+
assertThat(metadata).has(enabledFlag("incremental", true));
643+
assertThat(metadata).has(jmxEnabledFlag("incremental", true));
644+
assertThat(metadata).has(cacheTtl("incremental"));
645+
assertThat(metadata.getItems()).hasSize(4);
646+
project.replaceText(IncrementalJmxEndpoint.class, ", types = Endpoint.Type.JMX",
647+
"");
648+
metadata = project.incrementalBuild(IncrementalJmxEndpoint.class);
649+
assertThat(metadata).has(Metadata.withGroup("endpoints.incremental")
650+
.fromSource(IncrementalJmxEndpoint.class));
651+
assertThat(metadata).has(enabledFlag("incremental", true));
652+
assertThat(metadata).has(jmxEnabledFlag("incremental", true));
653+
assertThat(metadata).has(webEnabledFlag("incremental", true));
654+
assertThat(metadata).has(cacheTtl("incremental"));
655+
assertThat(metadata.getItems()).hasSize(5);
656+
}
657+
658+
private Metadata.MetadataItemCondition enabledFlag(String endpointId,
659+
boolean defaultValue) {
660+
return Metadata.withEnabledFlag("endpoints." + endpointId + ".enabled")
661+
.withDefaultValue(defaultValue).withDescription(
662+
String.format("Enable the %s endpoint.", endpointId));
663+
}
664+
665+
private Metadata.MetadataItemCondition jmxEnabledFlag(String endpointId,
666+
boolean defaultValue) {
667+
return Metadata.withEnabledFlag("endpoints." + endpointId + ".jmx.enabled")
668+
.withDefaultValue(defaultValue).withDescription(String.format(
669+
"Expose the %s endpoint as a JMX MBean.", endpointId));
670+
}
671+
672+
private Metadata.MetadataItemCondition webEnabledFlag(String endpointId,
673+
boolean defaultValue) {
674+
return Metadata.withEnabledFlag("endpoints." + endpointId + ".web.enabled")
675+
.withDefaultValue(defaultValue).withDescription(String.format(
676+
"Expose the %s endpoint as a Web endpoint.", endpointId));
677+
}
678+
679+
private Metadata.MetadataItemCondition cacheTtl(String endpointId) {
680+
return Metadata.withProperty("endpoints." + endpointId + ".cache.time-to-live")
681+
.ofType(Long.class).withDefaultValue(0).withDescription(
682+
"Maximum time in milliseconds that a response can be cached.");
683+
}
684+
520685
@Test
521686
public void mergingOfAdditionalProperty() throws Exception {
522687
ItemMetadata property = ItemMetadata.newProperty(null, "foo", "java.lang.String",

spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/TestConfigurationMetadataAnnotationProcessor.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ public class TestConfigurationMetadataAnnotationProcessor
4646

4747
static final String DEPRECATED_CONFIGURATION_PROPERTY_ANNOTATION = "org.springframework.boot.configurationsample.DeprecatedConfigurationProperty";
4848

49+
static final String ENDPOINT_ANNOTATION = "org.springframework.boot.configurationsample.Endpoint";
50+
4951
private ConfigurationMetadata metadata;
5052

5153
private final File outputLocation;
@@ -69,6 +71,11 @@ protected String deprecatedConfigurationPropertyAnnotation() {
6971
return DEPRECATED_CONFIGURATION_PROPERTY_ANNOTATION;
7072
}
7173

74+
@Override
75+
protected String endpointAnnotation() {
76+
return ENDPOINT_ANNOTATION;
77+
}
78+
7279
@Override
7380
protected ConfigurationMetadata writeMetaData() throws Exception {
7481
super.writeMetaData();

spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/metadata/Metadata.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ public static MetadataItemCondition withProperty(String name, String type) {
6262
return new MetadataItemCondition(ItemType.PROPERTY, name).ofType(type);
6363
}
6464

65+
public static Metadata.MetadataItemCondition withEnabledFlag(String key) {
66+
return withProperty(key).ofType(Boolean.class);
67+
}
68+
6569
public static MetadataHintCondition withHint(String name) {
6670
return new MetadataHintCondition(name);
6771
}

0 commit comments

Comments
 (0)