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 *     http://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 * @since 2.0
051 * @param <T> the concrete type of {@code Configuration} objects created by this builder
052 */
053public class FileBasedConfigurationBuilder<T extends FileBasedConfiguration> extends BasicConfigurationBuilder<T> {
054    /** A map for storing default encodings for specific configuration classes. */
055    private static final Map<Class<?>, String> DEFAULT_ENCODINGS = initializeDefaultEncodings();
056
057    /**
058     * Gets the default encoding for the specified configuration class. If an encoding has been set for the specified
059     * class (or one of its super classes), it is returned. Otherwise, result is <b>null</b>.
060     *
061     * @param configClass the configuration class in question
062     * @return the default encoding for this class (may be <b>null</b>)
063     */
064    public static String getDefaultEncoding(final Class<?> configClass) {
065        String enc = DEFAULT_ENCODINGS.get(configClass);
066        if (enc != null || configClass == null) {
067            return enc;
068        }
069
070        for (final Class<?> cls : ClassUtils.getAllSuperclasses(configClass)) {
071            enc = DEFAULT_ENCODINGS.get(cls);
072            if (enc != null) {
073                return enc;
074            }
075        }
076
077        for (final Class<?> cls : ClassUtils.getAllInterfaces(configClass)) {
078            enc = DEFAULT_ENCODINGS.get(cls);
079            if (enc != null) {
080                return enc;
081            }
082        }
083
084        return null;
085    }
086
087    /**
088     * Creates a map with default encodings for configuration classes and populates it with default entries.
089     *
090     * @return the map with default encodings
091     */
092    private static Map<Class<?>, String> initializeDefaultEncodings() {
093        final Map<Class<?>, String> enc = new ConcurrentHashMap<>();
094        enc.put(PropertiesConfiguration.class, PropertiesConfiguration.DEFAULT_ENCODING);
095        enc.put(XMLPropertiesConfiguration.class, XMLPropertiesConfiguration.DEFAULT_ENCODING);
096        return enc;
097    }
098
099    /**
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     * <b>null</b>, it is removed.
104     *
105     * @param configClass the name of the configuration class (must not be <b>null</b>)
106     * @param encoding the default encoding for this class
107     * @throws IllegalArgumentException if the class is <b>null</b>
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 <b>null</b>
134     * @throws IllegalArgumentException if the result class is <b>null</b>
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 <b>null</b>
145     * @param params a map with initialization parameters
146     * @throws IllegalArgumentException if the result class is <b>null</b>
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 <b>null</b>
157     * @param params a map with initialization parameters
158     * @param allowFailOnInit the <em>allowFailOnInit</em> flag
159     * @throws IllegalArgumentException if the result class is <b>null</b>
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 <b>null</b>.
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 <b>true</b> if auto save is enabled, <b>false</b> 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 <b>true</b> if auto save mode is to be enabled, <b>false</b> 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}