Skip to content

Commit 26df557

Browse files
committed
Merge enablement and exposure conditions for Actuator endpoints
This commit merges the conditions for determining if an endpoint is available in a single condition, deprecating `ConditionalOnEnabledEndpoint` in the process. Closes gh-16169
1 parent 275d794 commit 26df557

File tree

33 files changed

+241
-207
lines changed

33 files changed

+241
-207
lines changed

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/audit/AuditEventsEndpointAutoConfiguration.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@
1818

1919
import org.springframework.boot.actuate.audit.AuditEventRepository;
2020
import org.springframework.boot.actuate.audit.AuditEventsEndpoint;
21-
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint;
22-
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnExposedEndpoint;
21+
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
2322
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
2423
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
2524
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
@@ -37,8 +36,7 @@
3736
*/
3837
@Configuration(proxyBeanMethods = false)
3938
@AutoConfigureAfter(AuditAutoConfiguration.class)
40-
@ConditionalOnEnabledEndpoint(endpoint = AuditEventsEndpoint.class)
41-
@ConditionalOnExposedEndpoint(endpoint = AuditEventsEndpoint.class)
39+
@ConditionalOnAvailableEndpoint(endpoint = AuditEventsEndpoint.class)
4240
public class AuditEventsEndpointAutoConfiguration {
4341

4442
@Bean

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/beans/BeansEndpointAutoConfiguration.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@
1616

1717
package org.springframework.boot.actuate.autoconfigure.beans;
1818

19-
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint;
20-
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnExposedEndpoint;
19+
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
2120
import org.springframework.boot.actuate.beans.BeansEndpoint;
2221
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
2322
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@@ -32,8 +31,7 @@
3231
* @since 2.0.0
3332
*/
3433
@Configuration(proxyBeanMethods = false)
35-
@ConditionalOnEnabledEndpoint(endpoint = BeansEndpoint.class)
36-
@ConditionalOnExposedEndpoint(endpoint = BeansEndpoint.class)
34+
@ConditionalOnAvailableEndpoint(endpoint = BeansEndpoint.class)
3735
public class BeansEndpointAutoConfiguration {
3836

3937
@Bean

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cache/CachesEndpointAutoConfiguration.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@
1818

1919
import java.util.Map;
2020

21-
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint;
22-
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnExposedEndpoint;
21+
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
2322
import org.springframework.boot.actuate.cache.CachesEndpoint;
2423
import org.springframework.boot.actuate.cache.CachesEndpointWebExtension;
2524
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
@@ -41,8 +40,7 @@
4140
*/
4241
@Configuration(proxyBeanMethods = false)
4342
@ConditionalOnClass(CacheManager.class)
44-
@ConditionalOnEnabledEndpoint(endpoint = CachesEndpoint.class)
45-
@ConditionalOnExposedEndpoint(endpoint = CachesEndpoint.class)
43+
@ConditionalOnAvailableEndpoint(endpoint = CachesEndpoint.class)
4644
@AutoConfigureAfter(CacheAutoConfiguration.class)
4745
public class CachesEndpointAutoConfiguration {
4846

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfiguration.java

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,7 @@
2828
import org.springframework.beans.factory.config.BeanPostProcessor;
2929
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryWebEndpointDiscoverer;
3030
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.servlet.CloudFoundryInfoEndpointWebExtension;
31-
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint;
32-
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnExposedEndpoint;
31+
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
3332
import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration;
3433
import org.springframework.boot.actuate.autoconfigure.info.InfoEndpointAutoConfiguration;
3534
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
@@ -86,8 +85,7 @@ public class ReactiveCloudFoundryActuatorAutoConfiguration {
8685

8786
@Bean
8887
@ConditionalOnMissingBean
89-
@ConditionalOnEnabledEndpoint
90-
@ConditionalOnExposedEndpoint
88+
@ConditionalOnAvailableEndpoint
9189
@ConditionalOnBean({ HealthEndpoint.class, ReactiveHealthEndpointWebExtension.class })
9290
public CloudFoundryReactiveHealthEndpointWebExtension cloudFoundryReactiveHealthEndpointWebExtension(
9391
ReactiveHealthEndpointWebExtension reactiveHealthEndpointWebExtension) {
@@ -97,8 +95,7 @@ public CloudFoundryReactiveHealthEndpointWebExtension cloudFoundryReactiveHealth
9795

9896
@Bean
9997
@ConditionalOnMissingBean
100-
@ConditionalOnEnabledEndpoint
101-
@ConditionalOnExposedEndpoint
98+
@ConditionalOnAvailableEndpoint
10299
@ConditionalOnBean({ InfoEndpoint.class, GitProperties.class })
103100
public CloudFoundryInfoEndpointWebExtension cloudFoundryInfoEndpointWebExtension(
104101
GitProperties properties, ObjectProvider<InfoContributor> infoContributors) {

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfiguration.java

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@
2525

2626
import org.springframework.beans.factory.ObjectProvider;
2727
import org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryWebEndpointDiscoverer;
28-
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint;
29-
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnExposedEndpoint;
28+
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
3029
import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration;
3130
import org.springframework.boot.actuate.autoconfigure.info.InfoEndpointAutoConfiguration;
3231
import org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementContextAutoConfiguration;
@@ -88,8 +87,7 @@ public class CloudFoundryActuatorAutoConfiguration {
8887

8988
@Bean
9089
@ConditionalOnMissingBean
91-
@ConditionalOnEnabledEndpoint
92-
@ConditionalOnExposedEndpoint
90+
@ConditionalOnAvailableEndpoint
9391
@ConditionalOnBean({ HealthEndpoint.class, HealthEndpointWebExtension.class })
9492
public CloudFoundryHealthEndpointWebExtension cloudFoundryHealthEndpointWebExtension(
9593
HealthEndpointWebExtension healthEndpointWebExtension) {
@@ -98,8 +96,7 @@ public CloudFoundryHealthEndpointWebExtension cloudFoundryHealthEndpointWebExten
9896

9997
@Bean
10098
@ConditionalOnMissingBean
101-
@ConditionalOnEnabledEndpoint
102-
@ConditionalOnExposedEndpoint
99+
@ConditionalOnAvailableEndpoint
103100
@ConditionalOnBean({ InfoEndpoint.class, GitProperties.class })
104101
public CloudFoundryInfoEndpointWebExtension cloudFoundryInfoEndpointWebExtension(
105102
GitProperties properties, ObjectProvider<InfoContributor> infoContributors) {

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/condition/ConditionsReportEndpointAutoConfiguration.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@
1616

1717
package org.springframework.boot.actuate.autoconfigure.condition;
1818

19-
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint;
20-
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnExposedEndpoint;
19+
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
2120
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
2221
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
2322
import org.springframework.boot.autoconfigure.condition.SearchStrategy;
@@ -33,8 +32,7 @@
3332
* @since 2.0.0
3433
*/
3534
@Configuration(proxyBeanMethods = false)
36-
@ConditionalOnEnabledEndpoint(endpoint = ConditionsReportEndpoint.class)
37-
@ConditionalOnExposedEndpoint(endpoint = ConditionsReportEndpoint.class)
35+
@ConditionalOnAvailableEndpoint(endpoint = ConditionsReportEndpoint.class)
3836
public class ConditionsReportEndpointAutoConfiguration {
3937

4038
@Bean

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/context/ShutdownEndpointAutoConfiguration.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@
1616

1717
package org.springframework.boot.actuate.autoconfigure.context;
1818

19-
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint;
20-
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnExposedEndpoint;
19+
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
2120
import org.springframework.boot.actuate.context.ShutdownEndpoint;
2221
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
2322
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@@ -31,8 +30,7 @@
3130
* @since 2.0.0
3231
*/
3332
@Configuration(proxyBeanMethods = false)
34-
@ConditionalOnEnabledEndpoint(endpoint = ShutdownEndpoint.class)
35-
@ConditionalOnExposedEndpoint(endpoint = ShutdownEndpoint.class)
33+
@ConditionalOnAvailableEndpoint(endpoint = ShutdownEndpoint.class)
3634
public class ShutdownEndpointAutoConfiguration {
3735

3836
@Bean

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/context/properties/ConfigurationPropertiesReportEndpointAutoConfiguration.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@
1616

1717
package org.springframework.boot.actuate.autoconfigure.context.properties;
1818

19-
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint;
20-
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnExposedEndpoint;
19+
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
2120
import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint;
2221
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
2322
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@@ -34,8 +33,7 @@
3433
* @since 2.0.0
3534
*/
3635
@Configuration(proxyBeanMethods = false)
37-
@ConditionalOnEnabledEndpoint(endpoint = ConfigurationPropertiesReportEndpoint.class)
38-
@ConditionalOnExposedEndpoint(endpoint = ConfigurationPropertiesReportEndpoint.class)
36+
@ConditionalOnAvailableEndpoint(endpoint = ConfigurationPropertiesReportEndpoint.class)
3937
@EnableConfigurationProperties(ConfigurationPropertiesReportEndpointProperties.class)
4038
public class ConfigurationPropertiesReportEndpointAutoConfiguration {
4139

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/AbstractEndpointCondition.java

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,28 @@
1616

1717
package org.springframework.boot.actuate.autoconfigure.endpoint.condition;
1818

19+
import java.lang.annotation.Annotation;
1920
import java.util.Map;
21+
import java.util.Optional;
2022

23+
import org.springframework.boot.actuate.endpoint.EndpointId;
2124
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
2225
import org.springframework.boot.actuate.endpoint.annotation.EndpointExtension;
26+
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
27+
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
2328
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
2429
import org.springframework.context.annotation.Bean;
2530
import org.springframework.context.annotation.ConditionContext;
2631
import org.springframework.core.annotation.AnnotationAttributes;
2732
import org.springframework.core.annotation.MergedAnnotation;
2833
import org.springframework.core.annotation.MergedAnnotations;
2934
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
35+
import org.springframework.core.env.Environment;
3036
import org.springframework.core.type.AnnotatedTypeMetadata;
3137
import org.springframework.core.type.MethodMetadata;
3238
import org.springframework.util.Assert;
3339
import org.springframework.util.ClassUtils;
40+
import org.springframework.util.ConcurrentReferenceHashMap;
3441

3542
/**
3643
* Base class for {@link Endpoint @Endpoint} related {@link SpringBootCondition}
@@ -42,12 +49,44 @@
4249
*/
4350
abstract class AbstractEndpointCondition extends SpringBootCondition {
4451

52+
private static final String ENABLED_BY_DEFAULT_KEY = "management.endpoints.enabled-by-default";
53+
54+
private static final ConcurrentReferenceHashMap<Environment, Optional<Boolean>> enabledByDefaultCache = new ConcurrentReferenceHashMap<>();
55+
4556
AnnotationAttributes getEndpointAttributes(Class<?> annotationClass,
4657
ConditionContext context, AnnotatedTypeMetadata metadata) {
4758
return getEndpointAttributes(getEndpointType(annotationClass, context, metadata));
4859
}
4960

50-
Class<?> getEndpointType(Class<?> annotationClass, ConditionContext context,
61+
protected ConditionOutcome getEnablementOutcome(ConditionContext context,
62+
AnnotatedTypeMetadata metadata, Class<? extends Annotation> annotationClass) {
63+
Environment environment = context.getEnvironment();
64+
AnnotationAttributes attributes = getEndpointAttributes(annotationClass, context,
65+
metadata);
66+
EndpointId id = EndpointId.of(attributes.getString("id"));
67+
String key = "management.endpoint." + id.toLowerCaseString() + ".enabled";
68+
Boolean userDefinedEnabled = environment.getProperty(key, Boolean.class);
69+
if (userDefinedEnabled != null) {
70+
return new ConditionOutcome(userDefinedEnabled,
71+
ConditionMessage.forCondition(annotationClass)
72+
.because("found property " + key + " with value "
73+
+ userDefinedEnabled));
74+
}
75+
Boolean userDefinedDefault = isEnabledByDefault(environment);
76+
if (userDefinedDefault != null) {
77+
return new ConditionOutcome(userDefinedDefault,
78+
ConditionMessage.forCondition(annotationClass)
79+
.because("no property " + key
80+
+ " found so using user defined default from "
81+
+ ENABLED_BY_DEFAULT_KEY));
82+
}
83+
boolean endpointDefault = attributes.getBoolean("enableByDefault");
84+
return new ConditionOutcome(endpointDefault,
85+
ConditionMessage.forCondition(annotationClass).because(
86+
"no property " + key + " found so using endpoint default"));
87+
}
88+
89+
protected Class<?> getEndpointType(Class<?> annotationClass, ConditionContext context,
5190
AnnotatedTypeMetadata metadata) {
5291
Map<String, Object> attributes = metadata
5392
.getAnnotationAttributes(annotationClass.getName());
@@ -73,7 +112,7 @@ Class<?> getEndpointType(Class<?> annotationClass, ConditionContext context,
73112
}
74113
}
75114

76-
AnnotationAttributes getEndpointAttributes(Class<?> type) {
115+
protected AnnotationAttributes getEndpointAttributes(Class<?> type) {
77116
MergedAnnotations annotations = MergedAnnotations.from(type,
78117
SearchStrategy.EXHAUSTIVE);
79118
MergedAnnotation<Endpoint> endpoint = annotations.get(Endpoint.class);
@@ -88,4 +127,14 @@ AnnotationAttributes getEndpointAttributes(Class<?> type) {
88127
return getEndpointAttributes(extension.getClass("endpoint"));
89128
}
90129

130+
private Boolean isEnabledByDefault(Environment environment) {
131+
Optional<Boolean> enabledByDefault = enabledByDefaultCache.get(environment);
132+
if (enabledByDefault == null) {
133+
enabledByDefault = Optional.ofNullable(
134+
environment.getProperty(ENABLED_BY_DEFAULT_KEY, Boolean.class));
135+
enabledByDefaultCache.put(environment, enabledByDefault);
136+
}
137+
return enabledByDefault.orElse(null);
138+
}
139+
91140
}
Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -28,28 +28,25 @@
2828
import org.springframework.core.env.Environment;
2929

3030
/**
31-
* {@link Conditional @Conditional} that checks whether an endpoint is exposed or not.
32-
* Matches according to the endpoint exposure configuration {@link Environment}
33-
* properties. This is designed as a companion annotation to
34-
* {@link ConditionalOnEnabledEndpoint @ConditionalOnEnabledEndpoint}.
31+
* {@link Conditional @Conditional} that checks whether an endpoint is available. An
32+
* endpoint is considered available if it is both enabled and exposed. Matches enablement
33+
* according to the endpoints specific {@link Environment} property, falling back to
34+
* {@code management.endpoints.enabled-by-default} or failing that
35+
* {@link Endpoint#enableByDefault()}. Matches exposure according to any of the
36+
* {@code management.endpoints.web.exposure.<id>} or
37+
* {@code management.endpoints.jmx.exposure.<id>} specific properties or failing that to
38+
* whether the application runs on
39+
* {@link org.springframework.boot.cloud.CloudPlatform#CLOUD_FOUNDRY}. Both those
40+
* conditions should match for the endpoint to be considered available.
3541
* <p>
36-
* For a given {@link Endpoint @Endpoint}, the condition will match if:
37-
* <ul>
38-
* <li>{@code "management.endpoints.web.exposure.*"} expose this endpoint</li>
39-
* <li>or if JMX is enabled and {@code "management.endpoints.jmx.exposure.*"} expose this
40-
* endpoint</li>
41-
* <li>or if the application is running on
42-
* {@link org.springframework.boot.cloud.CloudPlatform#CLOUD_FOUNDRY}</li>
43-
* </ul>
44-
*
4542
* When placed on a {@code @Bean} method, the endpoint defaults to the return type of the
4643
* factory method:
4744
*
4845
* <pre class="code">
4946
* &#064;Configuration
5047
* public class MyConfiguration {
5148
*
52-
* &#064;ConditionalOnExposedEndpoint
49+
* &#064;ConditionalOnAvailableEndpoint
5350
* &#064;Bean
5451
* public MyEndpoint myEndpoint() {
5552
* ...
@@ -63,7 +60,7 @@
6360
* &#064;Configuration
6461
* public class MyConfiguration {
6562
*
66-
* &#064;ConditionalOnExposedEndpoint
63+
* &#064;ConditionalOnAvailableEndpoint
6764
* &#064;Bean
6865
* public MyEndpointWebExtension myEndpointWebExtension() {
6966
* ...
@@ -72,8 +69,8 @@
7269
* }</pre>
7370
* <p>
7471
* In the sample above, {@code MyEndpointWebExtension} will be created if the endpoint is
75-
* enabled as defined by the rules above. {@code MyEndpointWebExtension} must be a regular
76-
* extension that refers to an endpoint, something like:
72+
* available as defined by the rules above. {@code MyEndpointWebExtension} must be a
73+
* regular extension that refers to an endpoint, something like:
7774
*
7875
* <pre class="code">
7976
* &#064;EndpointWebExtension(endpoint = MyEndpoint.class)
@@ -82,13 +79,13 @@
8279
* }</pre>
8380
* <p>
8481
* Alternatively, the target endpoint can be manually specified for components that should
85-
* only be created when a given endpoint is enabled:
82+
* only be created when a given endpoint is available:
8683
*
8784
* <pre class="code">
8885
* &#064;Configuration
8986
* public class MyConfiguration {
9087
*
91-
* &#064;ConditionalOnExposedEndpoint(endpoint = MyEndpoint.class)
88+
* &#064;ConditionalOnAvailableEndpoint(endpoint = MyEndpoint.class)
9289
* &#064;Bean
9390
* public MyComponent myComponent() {
9491
* ...
@@ -97,15 +94,15 @@
9794
* }</pre>
9895
*
9996
* @author Brian Clozel
97+
* @author Stephane Nicoll
10098
* @since 2.2.0
10199
* @see Endpoint
102-
* @see ConditionalOnEnabledEndpoint
103100
*/
104101
@Retention(RetentionPolicy.RUNTIME)
105102
@Target({ ElementType.METHOD, ElementType.TYPE })
106103
@Documented
107-
@Conditional(OnExposedEndpointCondition.class)
108-
public @interface ConditionalOnExposedEndpoint {
104+
@Conditional(OnAvailableEndpointCondition.class)
105+
public @interface ConditionalOnAvailableEndpoint {
109106

110107
/**
111108
* The endpoint type that should be checked. Inferred when the return type of the

0 commit comments

Comments
 (0)