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 }