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