001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *     https://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.configuration2.builder;
018
019import java.util.Map;
020import java.util.concurrent.ConcurrentHashMap;
021
022import org.apache.commons.configuration2.FileBasedConfiguration;
023import org.apache.commons.configuration2.PropertiesConfiguration;
024import org.apache.commons.configuration2.XMLPropertiesConfiguration;
025import org.apache.commons.configuration2.event.ConfigurationEvent;
026import org.apache.commons.configuration2.ex.ConfigurationException;
027import org.apache.commons.configuration2.io.FileHandler;
028import org.apache.commons.lang3.ClassUtils;
029import org.apache.commons.lang3.StringUtils;
030
031/**
032 * <p>
033 * A specialized {@code ConfigurationBuilder} implementation which can handle configurations read from a
034 * {@link FileHandler}.
035 * </p>
036 * <p>
037 * This class extends its base class by the support of a {@link FileBasedBuilderParametersImpl} object, and especially
038 * of the {@link FileHandler} contained in this object. When the builder creates a new object the resulting
039 * {@code Configuration} instance is associated with the {@code FileHandler}. If the {@code FileHandler} has a location
040 * set, the {@code Configuration} is directly loaded from this location.
041 * </p>
042 * <p>
043 * The {@code FileHandler} is kept by this builder and can be queried later on. It can be used for instance to save the
044 * current {@code Configuration} after it was modified. Some care has to be taken when changing the location of the
045 * {@code FileHandler}: The new location is recorded and also survives an invocation of the {@code resetResult()}
046 * method. However, when the builder's initialization parameters are reset by calling {@code resetParameters()} the
047 * location is reset, too.
048 * </p>
049 *
050 * @param <T> the concrete type of {@code Configuration} objects created by this builder
051 * @since 2.0
052 */
053public class FileBasedConfigurationBuilder<T extends FileBasedConfiguration> extends BasicConfigurationBuilder<T> {
054
055    /** A map for storing default encodings for specific configuration classes. */
056    private static final Map<Class<?>, String> DEFAULT_ENCODINGS = initializeDefaultEncodings();
057
058    /**
059     * Gets the default encoding for the specified configuration class. If an encoding has been set for the specified
060     * class (or one of its super classes), it is returned. Otherwise, result is <strong>null</strong>.
061     *
062     * @param configClass the configuration class in question
063     * @return the default encoding for this class (may be <strong>null</strong>)
064     */
065    public static String getDefaultEncoding(final Class<?> configClass) {
066        String enc = DEFAULT_ENCODINGS.get(configClass);
067        if (enc != null || configClass == null) {
068            return enc;
069        }
070
071        for (final Class<?> cls : ClassUtils.getAllSuperclasses(configClass)) {
072            enc = DEFAULT_ENCODINGS.get(cls);
073            if (enc != null) {
074                return enc;
075            }
076        }
077
078        for (final Class<?> cls : ClassUtils.getAllInterfaces(configClass)) {
079            enc = DEFAULT_ENCODINGS.get(cls);
080            if (enc != null) {
081                return enc;
082            }
083        }
084
085        return null;
086    }
087
088    /**
089     * Creates a map with default encodings for configuration classes and populates it with default entries.
090     *
091     * @return the map with default encodings
092     */
093    private static Map<Class<?>, String> initializeDefaultEncodings() {
094        final Map<Class<?>, String> enc = new ConcurrentHashMap<>();
095        enc.put(PropertiesConfiguration.class, PropertiesConfiguration.DEFAULT_ENCODING);
096        enc.put(XMLPropertiesConfiguration.class, XMLPropertiesConfiguration.DEFAULT_ENCODING);
097        return enc;
098    }
099
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}