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 }