1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17 package org.apache.commons.configuration2.builder.combined;
18
19 import java.lang.reflect.Constructor;
20 import java.util.ArrayList;
21 import java.util.Collection;
22 import java.util.Collections;
23 import java.util.Map;
24
25 import org.apache.commons.configuration2.Configuration;
26 import org.apache.commons.configuration2.ConfigurationUtils;
27 import org.apache.commons.configuration2.builder.BasicConfigurationBuilder;
28 import org.apache.commons.configuration2.builder.BuilderParameters;
29 import org.apache.commons.configuration2.builder.ConfigurationBuilder;
30 import org.apache.commons.configuration2.ex.ConfigurationException;
31
32 /**
33 * <p>
34 * A fully-functional, reflection-based implementation of the {@code ConfigurationBuilderProvider} interface which can
35 * deal with the default tags defining configuration sources.
36 * </p>
37 * <p>
38 * An instance of this class is initialized with the names of the {@code ConfigurationBuilder} class used by this
39 * provider and the concrete {@code Configuration} class. The {@code ConfigurationBuilder} class must be derived from
40 * {@link BasicConfigurationBuilder}. When asked for the builder object, an instance of the builder class is created and
41 * initialized from the bean declaration associated with the current configuration source.
42 * </p>
43 * <p>
44 * {@code ConfigurationBuilder} objects are configured using parameter objects. When declaring configuration sources in
45 * XML it should not be necessary to define the single parameter objects. Rather, simple and complex properties are set
46 * in the typical way of a bean declaration (i.e. as attributes of the current XML element or as child elements). This
47 * class creates all supported parameter objects (whose names also must be provided at construction time) and takes care
48 * that their properties are initialized according to the current bean declaration.
49 * </p>
50 * <p>
51 * The use of reflection to create builder instances allows a generic implementation supporting many concrete builder
52 * classes. Another reason for this approach is that builder classes are only loaded if actually needed. Some
53 * specialized {@code Configuration} implementations require specific external dependencies which should not be
54 * mandatory for the use of {@code CombinedConfigurationBuilder}. Because such classes are lazily loaded, an application
55 * only has to include the dependencies it actually uses.
56 * </p>
57 *
58 * @since 2.0
59 */
60 public class BaseConfigurationBuilderProvider implements ConfigurationBuilderProvider {
61
62 /** The types of the constructor parameters for a basic builder. */
63 private static final Class<?>[] CTOR_PARAM_TYPES = {Class.class, Map.class, Boolean.TYPE};
64
65 /**
66 * Creates an instance of a parameter class using reflection.
67 *
68 * @param paramcls the parameter class
69 * @return the newly created instance
70 * @throws Exception if an error occurs
71 */
72 private static BuilderParameters createParameterObject(final String paramcls) throws ReflectiveOperationException {
73 return (BuilderParameters) ConfigurationUtils.loadClass(paramcls).getConstructor().newInstance();
74 }
75
76 /**
77 * Creates a new, unmodifiable collection for the parameter classes.
78 *
79 * @param paramCls the collection with parameter classes passed to the constructor
80 * @return the collection to be stored
81 */
82 private static Collection<String> initParameterClasses(final Collection<String> paramCls) {
83 if (paramCls == null) {
84 return Collections.emptySet();
85 }
86 return Collections.unmodifiableCollection(new ArrayList<>(paramCls));
87 }
88
89 /** The name of the builder class. */
90 private final String builderClass;
91
92 /** The name of a builder class with reloading support. */
93 private final String reloadingBuilderClass;
94
95 /** Stores the name of the configuration class to be created. */
96 private final String configurationClass;
97
98 /** A collection with the names of parameter classes. */
99 private final Collection<String> parameterClasses;
100
101 /**
102 * Creates a new instance of {@code BaseConfigurationBuilderProvider} and initializes all its properties.
103 *
104 * @param bldrCls the name of the builder class (must not be <strong>null</strong>)
105 * @param reloadBldrCls the name of a builder class to be used if reloading support is required (<strong>null</strong> if
106 * reloading is not supported)
107 * @param configCls the name of the configuration class (must not be <strong>null</strong>)
108 * @param paramCls a collection with the names of parameters classes
109 * @throws IllegalArgumentException if a required parameter is missing
110 */
111 public BaseConfigurationBuilderProvider(final String bldrCls, final String reloadBldrCls, final String configCls, final Collection<String> paramCls) {
112 if (bldrCls == null) {
113 throw new IllegalArgumentException("Builder class must not be null.");
114 }
115 if (configCls == null) {
116 throw new IllegalArgumentException("Configuration class must not be null.");
117 }
118
119 builderClass = bldrCls;
120 reloadingBuilderClass = reloadBldrCls;
121 configurationClass = configCls;
122 parameterClasses = initParameterClasses(paramCls);
123 }
124
125 /**
126 * Configures a newly created builder instance with its initialization parameters. This method is called after a new
127 * instance was created using reflection. This implementation passes the parameter objects to the builder's
128 * {@code configure()} method.
129 *
130 * @param builder the builder to be initialized
131 * @param decl the current {@code ConfigurationDeclaration}
132 * @param params the collection with initialization parameter objects
133 * @throws Exception if an error occurs
134 */
135 protected void configureBuilder(final BasicConfigurationBuilder<? extends Configuration> builder, final ConfigurationDeclaration decl,
136 final Collection<BuilderParameters> params) throws Exception {
137 builder.configure(params.toArray(new BuilderParameters[params.size()]));
138 }
139
140 /**
141 * Creates a new, uninitialized instance of the builder class managed by this provider. This implementation determines
142 * the builder class to be used by delegating to {@code determineBuilderClass()}. It then calls the constructor
143 * expecting the configuration class, the map with properties, and the<em>allowFailOnInit</em> flag.
144 *
145 * @param decl the current {@code ConfigurationDeclaration}
146 * @param params initialization parameters for the new builder object
147 * @return the newly created builder instance
148 * @throws Exception if an error occurs
149 */
150 protected BasicConfigurationBuilder<? extends Configuration> createBuilder(final ConfigurationDeclaration decl, final Collection<BuilderParameters> params)
151 throws Exception {
152 final Class<?> bldCls = ConfigurationUtils.loadClass(determineBuilderClass(decl));
153 final Class<?> configCls = ConfigurationUtils.loadClass(determineConfigurationClass(decl, params));
154 final Constructor<?> ctor = bldCls.getConstructor(CTOR_PARAM_TYPES);
155 // ? extends Configuration is the minimum constraint
156 @SuppressWarnings("unchecked")
157 final BasicConfigurationBuilder<? extends Configuration> builder = (BasicConfigurationBuilder<? extends Configuration>) ctor.newInstance(configCls,
158 null, isAllowFailOnInit(decl));
159 return builder;
160 }
161
162 /**
163 * Creates a collection of parameter objects to be used for configuring the builder. This method creates instances of
164 * the parameter classes passed to the constructor.
165 *
166 * @return a collection with parameter objects for the builder
167 * @throws Exception if an error occurs while creating parameter objects via reflection
168 */
169 protected Collection<BuilderParameters> createParameterObjects() throws Exception {
170 final Collection<BuilderParameters> params = new ArrayList<>(getParameterClasses().size());
171 for (final String paramcls : getParameterClasses()) {
172 params.add(createParameterObject(paramcls));
173 }
174 return params;
175 }
176
177 /**
178 * Determines the name of the class to be used for a new builder instance. This implementation selects between the
179 * normal and the reloading builder class, based on the passed in {@code ConfigurationDeclaration}. If a reloading
180 * builder is desired, but this provider has no reloading support, an exception is thrown.
181 *
182 * @param decl the current {@code ConfigurationDeclaration}
183 * @return the name of the builder class
184 * @throws ConfigurationException if the builder class cannot be determined
185 */
186 protected String determineBuilderClass(final ConfigurationDeclaration decl) throws ConfigurationException {
187 if (decl.isReload()) {
188 if (getReloadingBuilderClass() == null) {
189 throw new ConfigurationException("No support for reloading for builder class %s", getBuilderClass());
190 }
191 return getReloadingBuilderClass();
192 }
193 return getBuilderClass();
194 }
195
196 /**
197 * Determines the name of the configuration class produced by the builder. This method is called when obtaining the
198 * arguments for invoking the constructor of the builder class. This implementation just returns the pre-configured
199 * configuration class name. Derived classes may determine this class name dynamically based on the passed in
200 * parameters.
201 *
202 * @param decl the current {@code ConfigurationDeclaration}
203 * @param params the collection with parameter objects
204 * @return the name of the builder's result configuration class
205 * @throws ConfigurationException if an error occurs
206 */
207 protected String determineConfigurationClass(final ConfigurationDeclaration decl, final Collection<BuilderParameters> params)
208 throws ConfigurationException {
209 return getConfigurationClass();
210 }
211
212 /**
213 * Gets the name of the class of the builder created by this provider.
214 *
215 * @return the builder class
216 */
217 public String getBuilderClass() {
218 return builderClass;
219 }
220
221 /**
222 * {@inheritDoc} This implementation delegates to some protected methods to create a new builder instance using
223 * reflection and to configure it with parameter values defined by the passed in {@code BeanDeclaration}.
224 */
225 @Override
226 public ConfigurationBuilder<? extends Configuration> getConfigurationBuilder(final ConfigurationDeclaration decl) throws ConfigurationException {
227 try {
228 final Collection<BuilderParameters> params = createParameterObjects();
229 initializeParameterObjects(decl, params);
230 final BasicConfigurationBuilder<? extends Configuration> builder = createBuilder(decl, params);
231 configureBuilder(builder, decl, params);
232 return builder;
233 } catch (final ConfigurationException cex) {
234 throw cex;
235 } catch (final Exception ex) {
236 throw new ConfigurationException(ex);
237 }
238 }
239
240 /**
241 * Gets the name of the configuration class created by the builder produced by this provider.
242 *
243 * @return the configuration class
244 */
245 public String getConfigurationClass() {
246 return configurationClass;
247 }
248
249 /**
250 * Gets an unmodifiable collection with the names of parameter classes supported by this provider.
251 *
252 * @return the parameter classes
253 */
254 public Collection<String> getParameterClasses() {
255 return parameterClasses;
256 }
257
258 /**
259 * Gets the name of the class of the builder created by this provider if the reload flag is set. If this method
260 * returns <strong>null</strong>, reloading builders are not supported by this provider.
261 *
262 * @return the reloading builder class
263 */
264 public String getReloadingBuilderClass() {
265 return reloadingBuilderClass;
266 }
267
268 /**
269 * Passes all parameter objects to the parent {@code CombinedConfigurationBuilder} so that properties already defined
270 * for the parent builder can be added. This method is called before the parameter objects are initialized from the
271 * definition configuration. This way properties from the parent builder are inherited, but can be overridden for child
272 * configurations.
273 *
274 * @param decl the current {@code ConfigurationDeclaration}
275 * @param params the collection with (uninitialized) parameter objects
276 */
277 protected void inheritParentBuilderProperties(final ConfigurationDeclaration decl, final Collection<BuilderParameters> params) {
278 params.forEach(p -> decl.getConfigurationBuilder().initChildBuilderParameters(p));
279 }
280
281 /**
282 * Initializes the parameter objects with data stored in the current bean declaration. This method is called before the
283 * newly created builder instance is configured with the parameter objects. It maps attributes of the bean declaration
284 * to properties of parameter objects. In addition, it invokes the parent {@code CombinedConfigurationBuilder} so that
285 * the parameters object can inherit properties already defined for this builder.
286 *
287 * @param decl the current {@code ConfigurationDeclaration}
288 * @param params the collection with (uninitialized) parameter objects
289 * @throws Exception if an error occurs
290 */
291 protected void initializeParameterObjects(final ConfigurationDeclaration decl, final Collection<BuilderParameters> params) throws Exception {
292 inheritParentBuilderProperties(decl, params);
293 final MultiWrapDynaBean wrapBean = new MultiWrapDynaBean(params);
294 decl.getConfigurationBuilder().initBean(wrapBean, decl);
295 }
296
297 /**
298 * Determines the <em>allowFailOnInit</em> flag for the newly created builder based on the given
299 * {@code ConfigurationDeclaration}. Some combinations of flags in the declaration say that a configuration source is
300 * optional, but an empty instance should be created if its creation fail.
301 *
302 * @param decl the current {@code ConfigurationDeclaration}
303 * @return the value of the <em>allowFailOnInit</em> flag
304 */
305 protected boolean isAllowFailOnInit(final ConfigurationDeclaration decl) {
306 return decl.isOptional() && decl.isForceCreate();
307 }
308 }