View Javadoc
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 }