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;
18  
19  import java.util.Map;
20  import java.util.concurrent.ConcurrentHashMap;
21  
22  import org.apache.commons.configuration2.FileBasedConfiguration;
23  import org.apache.commons.configuration2.PropertiesConfiguration;
24  import org.apache.commons.configuration2.XMLPropertiesConfiguration;
25  import org.apache.commons.configuration2.event.ConfigurationEvent;
26  import org.apache.commons.configuration2.ex.ConfigurationException;
27  import org.apache.commons.configuration2.io.FileHandler;
28  import org.apache.commons.lang3.ClassUtils;
29  import org.apache.commons.lang3.StringUtils;
30  
31  /**
32   * <p>
33   * A specialized {@code ConfigurationBuilder} implementation which can handle configurations read from a
34   * {@link FileHandler}.
35   * </p>
36   * <p>
37   * This class extends its base class by the support of a {@link FileBasedBuilderParametersImpl} object, and especially
38   * of the {@link FileHandler} contained in this object. When the builder creates a new object the resulting
39   * {@code Configuration} instance is associated with the {@code FileHandler}. If the {@code FileHandler} has a location
40   * set, the {@code Configuration} is directly loaded from this location.
41   * </p>
42   * <p>
43   * The {@code FileHandler} is kept by this builder and can be queried later on. It can be used for instance to save the
44   * current {@code Configuration} after it was modified. Some care has to be taken when changing the location of the
45   * {@code FileHandler}: The new location is recorded and also survives an invocation of the {@code resetResult()}
46   * method. However, when the builder's initialization parameters are reset by calling {@code resetParameters()} the
47   * location is reset, too.
48   * </p>
49   *
50   * @param <T> the concrete type of {@code Configuration} objects created by this builder
51   * @since 2.0
52   */
53  public class FileBasedConfigurationBuilder<T extends FileBasedConfiguration> extends BasicConfigurationBuilder<T> {
54      /** A map for storing default encodings for specific configuration classes. */
55      private static final Map<Class<?>, String> DEFAULT_ENCODINGS = initializeDefaultEncodings();
56  
57      /**
58       * Gets the default encoding for the specified configuration class. If an encoding has been set for the specified
59       * class (or one of its super classes), it is returned. Otherwise, result is <strong>null</strong>.
60       *
61       * @param configClass the configuration class in question
62       * @return the default encoding for this class (may be <strong>null</strong>)
63       */
64      public static String getDefaultEncoding(final Class<?> configClass) {
65          String enc = DEFAULT_ENCODINGS.get(configClass);
66          if (enc != null || configClass == null) {
67              return enc;
68          }
69  
70          for (final Class<?> cls : ClassUtils.getAllSuperclasses(configClass)) {
71              enc = DEFAULT_ENCODINGS.get(cls);
72              if (enc != null) {
73                  return enc;
74              }
75          }
76  
77          for (final Class<?> cls : ClassUtils.getAllInterfaces(configClass)) {
78              enc = DEFAULT_ENCODINGS.get(cls);
79              if (enc != null) {
80                  return enc;
81              }
82          }
83  
84          return null;
85      }
86  
87      /**
88       * Creates a map with default encodings for configuration classes and populates it with default entries.
89       *
90       * @return the map with default encodings
91       */
92      private static Map<Class<?>, String> initializeDefaultEncodings() {
93          final Map<Class<?>, String> enc = new ConcurrentHashMap<>();
94          enc.put(PropertiesConfiguration.class, PropertiesConfiguration.DEFAULT_ENCODING);
95          enc.put(XMLPropertiesConfiguration.class, XMLPropertiesConfiguration.DEFAULT_ENCODING);
96          return enc;
97      }
98  
99      /**
100      * Sets a default encoding for a specific configuration class. This encoding is used if an instance of this
101      * configuration class is to be created and no encoding has been set in the parameters object for this builder. The
102      * encoding passed here not only applies to the specified class but also to its sub classes. If the encoding is
103      * <strong>null</strong>, it is removed.
104      *
105      * @param configClass the name of the configuration class (must not be <strong>null</strong>)
106      * @param encoding the default encoding for this class
107      * @throws IllegalArgumentException if the class is <strong>null</strong>
108      */
109     public static void setDefaultEncoding(final Class<?> configClass, final String encoding) {
110         if (configClass == null) {
111             throw new IllegalArgumentException("Configuration class must not be null!");
112         }
113 
114         if (encoding == null) {
115             DEFAULT_ENCODINGS.remove(configClass);
116         } else {
117             DEFAULT_ENCODINGS.put(configClass, encoding);
118         }
119     }
120 
121     /** Stores the FileHandler associated with the current configuration. */
122     private FileHandler currentFileHandler;
123 
124     /** A specialized listener for the auto save mechanism. */
125     private AutoSaveListener autoSaveListener;
126 
127     /** A flag whether the builder's parameters were reset. */
128     private boolean resetParameters;
129 
130     /**
131      * Creates a new instance of {@code FileBasedConfigurationBuilder} which produces result objects of the specified class.
132      *
133      * @param resCls the result class (must not be <strong>null</strong>
134      * @throws IllegalArgumentException if the result class is <strong>null</strong>
135      */
136     public FileBasedConfigurationBuilder(final Class<? extends T> resCls) {
137         super(resCls);
138     }
139 
140     /**
141      * Creates a new instance of {@code FileBasedConfigurationBuilder} which produces result objects of the specified class
142      * and sets initialization parameters.
143      *
144      * @param resCls the result class (must not be <strong>null</strong>
145      * @param params a map with initialization parameters
146      * @throws IllegalArgumentException if the result class is <strong>null</strong>
147      */
148     public FileBasedConfigurationBuilder(final Class<? extends T> resCls, final Map<String, Object> params) {
149         super(resCls, params);
150     }
151 
152     /**
153      * Creates a new instance of {@code FileBasedConfigurationBuilder} which produces result objects of the specified class
154      * and sets initialization parameters and the <em>allowFailOnInit</em> flag.
155      *
156      * @param resCls the result class (must not be <strong>null</strong>
157      * @param params a map with initialization parameters
158      * @param allowFailOnInit the <em>allowFailOnInit</em> flag
159      * @throws IllegalArgumentException if the result class is <strong>null</strong>
160      */
161     public FileBasedConfigurationBuilder(final Class<? extends T> resCls, final Map<String, Object> params, final boolean allowFailOnInit) {
162         super(resCls, params, allowFailOnInit);
163     }
164 
165     /**
166      * {@inheritDoc} This method is overridden here to change the result type.
167      */
168     @Override
169     public FileBasedConfigurationBuilder<T> configure(final BuilderParameters... params) {
170         super.configure(params);
171         return this;
172     }
173 
174     /**
175      * Obtains the {@code FileHandler} from this builder's parameters. If no {@code FileBasedBuilderParametersImpl} object
176      * is found in this builder's parameters, a new one is created now and stored. This makes it possible to change the
177      * location of the associated file even if no parameters object was provided.
178      *
179      * @return the {@code FileHandler} from initialization parameters
180      */
181     private FileHandler fetchFileHandlerFromParameters() {
182         FileBasedBuilderParametersImpl fileParams = FileBasedBuilderParametersImpl.fromParameters(getParameters(), false);
183         if (fileParams == null) {
184             fileParams = new FileBasedBuilderParametersImpl();
185             addParameters(fileParams.getParameters());
186         }
187         return fileParams.getFileHandler();
188     }
189 
190     /**
191      * Gets the {@code FileHandler} associated with this builder. If already a result object has been created, this
192      * {@code FileHandler} can be used to save it. Otherwise, the {@code FileHandler} from the initialization parameters is
193      * returned (which is not associated with a {@code FileBased} object). Result is never <strong>null</strong>.
194      *
195      * @return the {@code FileHandler} associated with this builder
196      */
197     public synchronized FileHandler getFileHandler() {
198         return currentFileHandler != null ? currentFileHandler : fetchFileHandlerFromParameters();
199     }
200 
201     /**
202      * Initializes the encoding of the specified file handler. If already an encoding is set, it is used. Otherwise, the
203      * default encoding for the result configuration class is obtained and set.
204      *
205      * @param handler the handler to be initialized
206      */
207     private void initEncoding(final FileHandler handler) {
208         if (StringUtils.isEmpty(handler.getEncoding())) {
209             final String encoding = getDefaultEncoding(getResultClass());
210             if (encoding != null) {
211                 handler.setEncoding(encoding);
212             }
213         }
214     }
215 
216     /**
217      * Initializes the new current {@code FileHandler}. When a new result object is created, a new {@code FileHandler} is
218      * created, too, and associated with the result object. This new handler is passed to this method. If a location is
219      * defined, the result object is loaded from this location. Note: This method is called from a synchronized block.
220      *
221      * @param handler the new current {@code FileHandler}
222      * @throws ConfigurationException if an error occurs
223      */
224     protected void initFileHandler(final FileHandler handler) throws ConfigurationException {
225         initEncoding(handler);
226         if (handler.isLocationDefined()) {
227             handler.locate();
228             handler.load();
229         }
230     }
231 
232     /**
233      * {@inheritDoc} This implementation deals with the creation and initialization of a {@code FileHandler} associated with
234      * the new result object.
235      */
236     @Override
237     protected void initResultInstance(final T obj) throws ConfigurationException {
238         super.initResultInstance(obj);
239         final FileHandler srcHandler = currentFileHandler != null && !resetParameters ? currentFileHandler : fetchFileHandlerFromParameters();
240         currentFileHandler = new FileHandler(obj, srcHandler);
241 
242         if (autoSaveListener != null) {
243             autoSaveListener.updateFileHandler(currentFileHandler);
244         }
245         initFileHandler(currentFileHandler);
246         resetParameters = false;
247     }
248 
249     /**
250      * Installs the listener for the auto save mechanism if it is not yet active.
251      */
252     private void installAutoSaveListener() {
253         if (autoSaveListener == null) {
254             autoSaveListener = new AutoSaveListener(this);
255             addEventListener(ConfigurationEvent.ANY, autoSaveListener);
256             autoSaveListener.updateFileHandler(getFileHandler());
257         }
258     }
259 
260     /**
261      * Gets a flag whether auto save mode is currently active.
262      *
263      * @return <strong>true</strong> if auto save is enabled, <strong>false</strong> otherwise
264      */
265     public synchronized boolean isAutoSave() {
266         return autoSaveListener != null;
267     }
268 
269     /**
270      * Removes the listener for the auto save mechanism if it is currently active.
271      */
272     private void removeAutoSaveListener() {
273         if (autoSaveListener != null) {
274             removeEventListener(ConfigurationEvent.ANY, autoSaveListener);
275             autoSaveListener.updateFileHandler(null);
276             autoSaveListener = null;
277         }
278     }
279 
280     /**
281      * Convenience method which saves the associated configuration. This method expects that the managed configuration has
282      * already been created and that a valid file location is available in the current {@code FileHandler}. The file handler
283      * is then used to store the configuration.
284      *
285      * @throws ConfigurationException if an error occurs
286      */
287     public void save() throws ConfigurationException {
288         getFileHandler().save();
289     }
290 
291     /**
292      * Enables or disables auto save mode. If auto save mode is enabled, every update of the managed configuration causes it
293      * to be saved automatically; so changes are directly written to disk.
294      *
295      * @param enabled <strong>true</strong> if auto save mode is to be enabled, <strong>false</strong> otherwise
296      */
297     public synchronized void setAutoSave(final boolean enabled) {
298         if (enabled) {
299             installAutoSaveListener();
300         } else {
301             removeAutoSaveListener();
302         }
303     }
304 
305     /**
306      * {@inheritDoc} This implementation just records the fact that new parameters have been set. This means that the next
307      * time a result object is created, the {@code FileHandler} has to be initialized from initialization parameters rather
308      * than reusing the existing one.
309      */
310     @Override
311     public synchronized BasicConfigurationBuilder<T> setParameters(final Map<String, Object> params) {
312         super.setParameters(params);
313         resetParameters = true;
314         return this;
315     }
316 }