    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}