Skip to content
This repository was archived by the owner on Feb 23, 2023. It is now read-only.

Commit 1d4db62

Browse files
committed
Harmonize build-time parsing of the bean factory
Previously, BuildTimeBeanDefinitionsRegistrar was adding configuration class parsing manually and triggered it to build the list of bean definitions. Only certain framework callbacks were invoked. This commit harmonizes its processing so that it is as close as possible to what the regular runtime context would do. As a result, additional callbacks are invoked at build time which had a subtle impact on how bean definition types were discovered. To alleviate with that, BeanClassBeanDefinitionPostProcessor makes sure to resolve the bean class early if necessary. Closes gh-1213
1 parent 2cb3075 commit 1d4db62

16 files changed

+245
-108
lines changed

spring-aot/src/main/java/org/springframework/aot/context/bootstrap/generator/DefaultBeanDefinitionSelector.java

+3
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ public DefaultBeanDefinitionSelector(List<String> excludeTypes) {
4949
// TODO Make a better split between the AOT and runtime parts otherwise too much reflection is required, so for now @Autowired on fields/setters and event listeners are not properly supported
5050
this.excludedBeanNames.add(AnnotationConfigUtils.AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME);
5151
this.excludedBeanNames.add(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME);
52+
53+
// Only used during configuration class parsing
54+
this.excludedBeanNames.add("org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory");
5255
}
5356

5457
@Override
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package org.springframework.context.annotation;
2+
3+
import org.springframework.beans.BeansException;
4+
import org.springframework.beans.factory.BeanFactory;
5+
import org.springframework.beans.factory.BeanFactoryAware;
6+
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
7+
import org.springframework.beans.factory.support.RootBeanDefinition;
8+
import org.springframework.core.Ordered;
9+
import org.springframework.core.annotation.Order;
10+
11+
/**
12+
* Ensure that {@link RootBeanDefinition#hasBeanClass()} can be safely used by bean
13+
* definition processors.
14+
*
15+
* @author Stephane Nicoll
16+
*/
17+
@Order(Ordered.HIGHEST_PRECEDENCE)
18+
class BeanClassBeanDefinitionPostProcessor implements BeanDefinitionPostProcessor, BeanFactoryAware {
19+
20+
private ClassLoader classLoader;
21+
22+
@Override
23+
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
24+
this.classLoader = ((ConfigurableBeanFactory) beanFactory).getBeanClassLoader();
25+
}
26+
27+
@Override
28+
public void postProcessBeanDefinition(String beanName, RootBeanDefinition beanDefinition) {
29+
if (!beanDefinition.hasBeanClass()) {
30+
try {
31+
beanDefinition.resolveBeanClass(this.classLoader);
32+
}
33+
catch (ClassNotFoundException ex) {
34+
// ignore
35+
}
36+
}
37+
}
38+
39+
}

spring-aot/src/main/java/org/springframework/context/annotation/BuildTimeBeanDefinitionsRegistrar.java

+33-44
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.util.LinkedHashMap;
2020
import java.util.List;
2121
import java.util.Map;
22+
import java.util.function.Predicate;
2223

2324
import org.apache.commons.logging.Log;
2425
import org.apache.commons.logging.LogFactory;
@@ -29,18 +30,22 @@
2930
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
3031
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
3132
import org.springframework.beans.factory.support.RootBeanDefinition;
33+
import org.springframework.context.support.ApplicationContextAccessor;
3234
import org.springframework.context.support.GenericApplicationContext;
3335
import org.springframework.core.io.support.SpringFactoriesLoader;
3436
import org.springframework.core.type.AnnotationMetadata;
3537
import org.springframework.util.Assert;
3638
import org.springframework.util.ClassUtils;
3739

3840
/**
39-
* Parse the {@link Configuration @Configuration classes} and provide the bean definitions
40-
* at build time.
41+
* Process a {@link GenericApplicationContext} refresh phase up to a point where all the
42+
* bean definitions have been created, but prior to actually creating bean instances.
43+
* <p/>
44+
* This is used at build time to get an overview of an application context.
4145
*
4246
* @author Stephane Nicoll
4347
* @see ConditionEvaluationStateReport
48+
* @see BeanDefinitionPostProcessor
4449
*/
4550
public class BuildTimeBeanDefinitionsRegistrar {
4651

@@ -56,54 +61,15 @@ public ConfigurableListableBeanFactory processBeanDefinitions(GenericApplication
5661
Assert.notNull(context, "Context must not be null");
5762
Assert.state(!context.isActive(), () -> "Context must not be active");
5863
if (logger.isDebugEnabled()) {
59-
logger.debug("Parsing configuration classes");
64+
logger.debug("Processing bean factory");
6065
}
61-
parseConfigurationClasses(context);
62-
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
63-
invokeBeanDefinitionRegistryPostProcessors(beanFactory);
64-
resolveBeanDefinitionTypes(beanFactory);
66+
ConfigurableListableBeanFactory beanFactory = ApplicationContextAccessor.prepareContext(context);
6567
postProcessBeanDefinitions(beanFactory);
68+
removeBeanDefinitionRegistryPostProcessors(beanFactory);
6669
registerImportOriginRegistryIfNecessary(beanFactory);
6770
return beanFactory;
6871
}
6972

70-
private void parseConfigurationClasses(GenericApplicationContext context) {
71-
ConfigurationClassPostProcessor configurationClassPostProcessor = new ConfigurationClassPostProcessor();
72-
configurationClassPostProcessor.setApplicationStartup(context.getApplicationStartup());
73-
configurationClassPostProcessor.setBeanClassLoader(context.getClassLoader());
74-
configurationClassPostProcessor.setEnvironment(context.getEnvironment());
75-
configurationClassPostProcessor.setResourceLoader(context);
76-
configurationClassPostProcessor.postProcessBeanFactory(context.getBeanFactory());
77-
}
78-
79-
private void invokeBeanDefinitionRegistryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
80-
BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
81-
Map<String, BeanDefinitionRegistryPostProcessor> candidates = beanFactory.getBeansOfType(BeanDefinitionRegistryPostProcessor.class, true, false);
82-
candidates.forEach((beanName, postProcessor) -> {
83-
postProcessor.postProcessBeanDefinitionRegistry(registry);
84-
if (beanFactory.containsBeanDefinition(beanName)) {
85-
BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);
86-
if (beanDefinition.getRole() == BeanDefinition.ROLE_INFRASTRUCTURE) {
87-
((BeanDefinitionRegistry) beanFactory).removeBeanDefinition(beanName);
88-
logger.debug("Removed " + BeanDefinitionRegistryPostProcessor.class.getSimpleName() + " with bean name " + beanName);
89-
}
90-
else {
91-
logger.warn(BeanDefinitionRegistryPostProcessor.class.getSimpleName() + " with bean name "
92-
+ beanName + " is going to be invoked again at runtime, set a role infrastructure to avoid this");
93-
}
94-
}
95-
});
96-
}
97-
98-
private void resolveBeanDefinitionTypes(ConfigurableListableBeanFactory beanFactory) {
99-
if (logger.isDebugEnabled()) {
100-
logger.debug("Resolving types for " + beanFactory.getBeanDefinitionCount() + " bean definitions");
101-
}
102-
for (String name : beanFactory.getBeanDefinitionNames()) {
103-
beanFactory.getType(name);
104-
}
105-
}
106-
10773
private void postProcessBeanDefinitions(ConfigurableListableBeanFactory beanFactory) {
10874
if (logger.isDebugEnabled()) {
10975
logger.debug("Post processing " + beanFactory.getBeanDefinitionCount() + " bean definitions");
@@ -118,6 +84,29 @@ private void postProcessBeanDefinitions(ConfigurableListableBeanFactory beanFact
11884
}
11985
}
12086

87+
private void removeBeanDefinitionRegistryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
88+
Map<String, BeanDefinitionRegistryPostProcessor> candidates = beanFactory.getBeansOfType(BeanDefinitionRegistryPostProcessor.class, true, false);
89+
candidates.forEach((beanName, postProcessor) -> {
90+
if (!removeBeanDefinition(beanFactory, beanName, (bd) -> bd.getRole() == BeanDefinition.ROLE_INFRASTRUCTURE)) {
91+
logger.warn(BeanDefinitionRegistryPostProcessor.class.getSimpleName() + " with bean name "
92+
+ beanName + " is going to be invoked again at runtime, set a role infrastructure to avoid this");
93+
}
94+
});
95+
}
96+
97+
private boolean removeBeanDefinition(ConfigurableListableBeanFactory beanFactory, String beanName,
98+
Predicate<BeanDefinition> condition) {
99+
if (beanFactory.containsBeanDefinition(beanName)) {
100+
BeanDefinition beanDefinition = beanFactory.getMergedBeanDefinition(beanName);
101+
if (condition.test(beanDefinition)) {
102+
((BeanDefinitionRegistry) beanFactory).removeBeanDefinition(beanName);
103+
logger.debug("Removed bean definition with name " + beanName);
104+
return true;
105+
}
106+
}
107+
return false;
108+
}
109+
121110
private void registerImportOriginRegistryIfNecessary(ConfigurableListableBeanFactory beanFactory) {
122111
if (logger.isDebugEnabled()) {
123112
logger.debug("Retrieving import origins if necessary");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package org.springframework.context.support;
2+
3+
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
4+
5+
/**
6+
* Accessor to privileged methods of {@link GenericApplicationContext}.
7+
*
8+
* @author Stephane Nicoll
9+
*/
10+
public class ApplicationContextAccessor {
11+
12+
/**
13+
* Prepare the specified {@link GenericApplicationContext} up to a point where it is
14+
* ready to create bean instances
15+
* @param context the context to prepare
16+
* @return the processed bean factory
17+
*/
18+
public static ConfigurableListableBeanFactory prepareContext(GenericApplicationContext context) {
19+
context.prepareRefresh();
20+
ConfigurableListableBeanFactory beanFactory = context.obtainFreshBeanFactory();
21+
context.prepareBeanFactory(beanFactory);
22+
context.postProcessBeanFactory(beanFactory);
23+
context.invokeBeanFactoryPostProcessors(beanFactory);
24+
context.registerBeanPostProcessors(beanFactory);
25+
return beanFactory;
26+
}
27+
28+
}

spring-aot/src/main/resources/META-INF/spring.factories

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
org.springframework.context.annotation.BeanDefinitionPostProcessor=\
2+
org.springframework.context.annotation.BeanClassBeanDefinitionPostProcessor,\
23
org.springframework.data.RepositoryFactoryBeanPostProcessor,\
34
org.springframework.plugin.PluginRegistryFactoryBeanPostProcessor,\
45
org.springframework.context.annotation.CommonAnnotationBeanDefinitionPostProcessor

spring-aot/src/test/java/org/springframework/aot/context/bootstrap/generator/event/EventListenerMethodRegistrationGeneratorTests.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ void writeEventListenersRegistrationWithEventListeners() {
103103

104104
@Test
105105
void writeEventListenersRegistrationWithPackageProtectedEventListener() {
106-
GenericApplicationContext context = new GenericApplicationContext(prepareBeanFactory());
106+
GenericApplicationContext context = new AnnotationConfigApplicationContext();
107107
context.registerBeanDefinition("configuration", BeanDefinitionBuilder.rootBeanDefinition(ProtectedEventListenerConfiguration.class)
108108
.getBeanDefinition());
109109
BuildTimeBeanDefinitionsRegistrar registrar = new BuildTimeBeanDefinitionsRegistrar();

spring-aot/src/test/java/org/springframework/aot/context/bootstrap/generator/infrastructure/BootstrapInfrastructureWriterTests.java

+23-18
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.springframework.aot.context.bootstrap.generator.sample.callback.NestedImportConfiguration;
3030
import org.springframework.aot.context.bootstrap.generator.test.CodeSnippet;
3131
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
32+
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
3233
import org.springframework.context.annotation.BuildTimeBeanDefinitionsRegistrar;
3334
import org.springframework.context.annotation.ContextAnnotationAutowireCandidateResolver;
3435
import org.springframework.context.annotation.samples.simple.ConfigurationTwo;
@@ -47,39 +48,39 @@ class BootstrapInfrastructureWriterTests {
4748

4849
@Test
4950
void writeInfrastructureSetAutowireCandidateResolver() {
50-
assertThat(writeInfrastructure(new GenericApplicationContext(), createBootstrapContext()))
51+
assertThat(writeInfrastructure(createBootstrapContext()))
5152
.contains("context.getDefaultListableBeanFactory().setAutowireCandidateResolver(new ContextAnnotationAutowireCandidateResolver());")
5253
.hasImport(ContextAnnotationAutowireCandidateResolver.class);
5354
}
5455

5556
@Test
5657
void writeInfrastructureWithNoImportAwareCandidateDoesNotRegisterBean() {
57-
assertThat(writeInfrastructure(new GenericApplicationContext(), createBootstrapContext()))
58+
assertThat(writeInfrastructure(createBootstrapContext()))
5859
.doesNotContain(ImportAwareBeanPostProcessor.class.getSimpleName());
5960
}
6061

6162
@Test
6263
void writeInfrastructureWithNoImportAwareMappingDoesNotRegisterBean() {
63-
GenericApplicationContext context = new GenericApplicationContext();
64+
GenericApplicationContext context = new AnnotationConfigApplicationContext();
6465
context.registerBean(ConfigurationTwo.class);
65-
assertThat(writeInfrastructure(context, createBootstrapContext()))
66+
assertThat(writeInfrastructure(createBootstrapContext(), context))
6667
.doesNotContain(ImportAwareBeanPostProcessor.class.getSimpleName());
6768
}
6869

6970
@Test
7071
void writeInfrastructureWithImportAwareRegisterBean() {
71-
GenericApplicationContext context = new GenericApplicationContext();
72+
GenericApplicationContext context = new AnnotationConfigApplicationContext();
7273
context.registerBean(ImportConfiguration.class);
73-
assertThat(writeInfrastructure(context, createBootstrapContext()))
74+
assertThat(writeInfrastructure(createBootstrapContext(), context))
7475
.contains("context.getBeanFactory().addBeanPostProcessor(createImportAwareBeanPostProcessor());");
7576
}
7677

7778
@Test
7879
void writeInfrastructureWithImportAwareRegisterCreateMethod() {
79-
GenericApplicationContext context = new GenericApplicationContext();
80+
GenericApplicationContext context = new AnnotationConfigApplicationContext();
8081
context.registerBean(ImportConfiguration.class);
8182
BootstrapWriterContext bootstrapContext = createBootstrapContext();
82-
writeInfrastructure(context, bootstrapContext);
83+
writeInfrastructure(bootstrapContext, context);
8384
assertThat(generateCode(bootstrapContext.getBootstrapClass("com.example")).lines()).contains(
8485
" private ImportAwareBeanPostProcessor createImportAwareBeanPostProcessor() {",
8586
" Map<String, String> mappings = new LinkedHashMap<>();",
@@ -92,11 +93,11 @@ void writeInfrastructureWithImportAwareRegisterCreateMethod() {
9293

9394
@Test
9495
void writeInfrastructureWithSeveralImportAwareInstances() {
95-
GenericApplicationContext context = new GenericApplicationContext();
96+
GenericApplicationContext context = new AnnotationConfigApplicationContext();
9697
context.registerBean(ImportConfiguration.class);
9798
context.registerBean(AsyncConfiguration.class);
9899
BootstrapWriterContext bootstrapContext = createBootstrapContext();
99-
writeInfrastructure(context, bootstrapContext);
100+
writeInfrastructure(bootstrapContext, context);
100101
assertThat(generateCode(bootstrapContext.getBootstrapClass("com.example")).lines()).contains(
101102
" private ImportAwareBeanPostProcessor createImportAwareBeanPostProcessor() {",
102103
" Map<String, String> mappings = new LinkedHashMap<>();",
@@ -111,10 +112,10 @@ void writeInfrastructureWithSeveralImportAwareInstances() {
111112

112113
@Test
113114
void writeInfrastructureWithNestedClass() {
114-
GenericApplicationContext context = new GenericApplicationContext();
115+
GenericApplicationContext context = new AnnotationConfigApplicationContext();
115116
context.registerBean(NestedImportConfiguration.class);
116117
BootstrapWriterContext bootstrapContext = createBootstrapContext();
117-
writeInfrastructure(context, bootstrapContext);
118+
writeInfrastructure(bootstrapContext, context);
118119
assertThat(generateCode(bootstrapContext.getBootstrapClass("com.example")).lines()).contains(
119120
" private ImportAwareBeanPostProcessor createImportAwareBeanPostProcessor() {",
120121
" Map<String, String> mappings = new LinkedHashMap<>();",
@@ -127,17 +128,17 @@ void writeInfrastructureWithNestedClass() {
127128

128129
@Test
129130
void writeInfrastructureWithNoLifecycleMethodsDoesNotRegisterBean() {
130-
assertThat(writeInfrastructure(new GenericApplicationContext(), createBootstrapContext()))
131+
assertThat(writeInfrastructure(createBootstrapContext()))
131132
.doesNotContain(InitDestroyBeanPostProcessor.class.getSimpleName())
132133
.doesNotContain("createInitDestroyBeanPostProcessor");
133134
}
134135

135136
@Test
136137
void writeInfrastructureWithLifecycleMethodsRegisterCreateMethod() {
137-
GenericApplicationContext context = new GenericApplicationContext();
138+
GenericApplicationContext context = new AnnotationConfigApplicationContext();
138139
context.registerBean("testBean", InitDestroySampleBean.class);
139140
BootstrapWriterContext bootstrapContext = createBootstrapContext();
140-
writeInfrastructure(context, bootstrapContext);
141+
writeInfrastructure(bootstrapContext, context);
141142
assertThat(generateCode(bootstrapContext.getBootstrapClass("com.example")).lines()).contains(
142143
" private InitDestroyBeanPostProcessor createInitDestroyBeanPostProcessor(",
143144
" ConfigurableBeanFactory beanFactory) {",
@@ -151,13 +152,17 @@ void writeInfrastructureWithLifecycleMethodsRegisterCreateMethod() {
151152

152153
@Test
153154
void writeInfrastructureWithLifecycleMethodsRegisterBean() {
154-
GenericApplicationContext context = new GenericApplicationContext();
155+
GenericApplicationContext context = new AnnotationConfigApplicationContext();
155156
context.registerBean("testBean", InitDestroySampleBean.class);
156-
assertThat(writeInfrastructure(context, createBootstrapContext())).contains(
157+
assertThat(writeInfrastructure(createBootstrapContext(), context)).contains(
157158
"context.getBeanFactory().addBeanPostProcessor(createInitDestroyBeanPostProcessor(context.getBeanFactory()));");
158159
}
159160

160-
private CodeSnippet writeInfrastructure(GenericApplicationContext context, BootstrapWriterContext writerContext) {
161+
private CodeSnippet writeInfrastructure(BootstrapWriterContext writerContext) {
162+
return writeInfrastructure(writerContext, new AnnotationConfigApplicationContext());
163+
}
164+
165+
private CodeSnippet writeInfrastructure(BootstrapWriterContext writerContext, GenericApplicationContext context) {
161166
ConfigurableListableBeanFactory beanFactory = this.registrar.processBeanDefinitions(context);
162167
BootstrapInfrastructureWriter writer = new BootstrapInfrastructureWriter(beanFactory, writerContext);
163168
return CodeSnippet.of(writer::writeInfrastructure);

spring-aot/src/test/java/org/springframework/aot/context/bootstrap/generator/test/ApplicationContextAotProcessorTester.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
import org.springframework.aot.context.bootstrap.generator.ApplicationContextAotProcessor;
2929
import org.springframework.aot.context.bootstrap.generator.infrastructure.DefaultBootstrapWriterContext;
30+
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
3031
import org.springframework.context.support.GenericApplicationContext;
3132
import org.springframework.util.ClassUtils;
3233

@@ -53,7 +54,7 @@ public ApplicationContextAotProcessorTester(Path directory) {
5354
}
5455

5556
public ContextBootstrapStructure process(Class<?>... candidates) {
56-
GenericApplicationContext context = new GenericApplicationContext();
57+
GenericApplicationContext context = new AnnotationConfigApplicationContext();
5758
for (Class<?> candidate : candidates) {
5859
context.registerBean(generateShortName(candidate), candidate);
5960
}

spring-aot/src/test/java/org/springframework/aot/context/origin/BeanFactoryStructureAnalyzerTests.java

+4-3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import org.junit.jupiter.api.Test;
2020

21+
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
2122
import org.springframework.context.annotation.BuildTimeBeanDefinitionsRegistrar;
2223
import org.springframework.context.annotation.samples.simple.ConfigurationOne;
2324
import org.springframework.context.support.GenericApplicationContext;
@@ -33,13 +34,13 @@ class BeanFactoryStructureAnalyzerTests {
3334

3435
@Test
3536
void analyzeSimpleStructure() {
36-
GenericApplicationContext context = new GenericApplicationContext();
37+
GenericApplicationContext context = new AnnotationConfigApplicationContext();
3738
context.registerBean(ConfigurationOne.class);
3839
BeanFactoryStructure structure = analyze(context);
3940
assertThat(structure).isNotNull();
40-
assertThat(structure.getDescriptors()).containsOnlyKeys("beanOne", ConfigurationOne.class.getName());
41+
assertThat(structure.getDescriptors()).containsKeys("beanOne", "configurationOne");
4142
assertThat(structure.getDescriptors().get("beanOne").getOrigins())
42-
.containsOnly(ConfigurationOne.class.getName());
43+
.containsOnly("configurationOne");
4344
}
4445

4546
private BeanFactoryStructure analyze(GenericApplicationContext context) {

spring-aot/src/test/java/org/springframework/boot/autoconfigure/AutoConfigurationPackagesBeanDefinitionOriginAnalyzerTests.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import org.springframework.aot.context.origin.BeanDefinitionDescriptor.Type;
2222
import org.springframework.aot.context.origin.BeanFactoryStructureAnalysis;
23+
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
2324
import org.springframework.context.annotation.BuildTimeBeanDefinitionsRegistrar;
2425
import org.springframework.context.annotation.Configuration;
2526
import org.springframework.context.support.GenericApplicationContext;
@@ -37,7 +38,7 @@ class AutoConfigurationPackagesBeanDefinitionOriginAnalyzerTests {
3738

3839
@Test
3940
void analyseAutoConfigurePackages() {
40-
GenericApplicationContext context = new GenericApplicationContext();
41+
GenericApplicationContext context = new AnnotationConfigApplicationContext();
4142
context.registerBean(SampleConfiguration.class);
4243
new BuildTimeBeanDefinitionsRegistrar().processBeanDefinitions(context);
4344
BeanFactoryStructureAnalysis analysis = BeanFactoryStructureAnalysis.of(context.getBeanFactory());

0 commit comments

Comments
 (0)