    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.List;
    020import java.util.Map;
    021import java.util.concurrent.ConcurrentHashMap;
    022
    023import org.apache.commons.configuration2.FileBasedConfiguration;
    024import org.apache.commons.configuration2.PropertiesConfiguration;
    025import org.apache.commons.configuration2.XMLPropertiesConfiguration;
    026import org.apache.commons.configuration2.event.ConfigurationEvent;
    027import org.apache.commons.configuration2.ex.ConfigurationException;
    028import org.apache.commons.configuration2.io.FileHandler;
    029import org.apache.commons.lang3.ClassUtils;
    030import org.apache.commons.lang3.StringUtils;
    031
    032/**
    033 * <p>
    034 * A specialized {@code ConfigurationBuilder} implementation which can handle
    035 * configurations read from a {@link FileHandler}.
    036 * </p>
    037 * <p>
    038 * This class extends its base class by the support of a
    039 * {@link FileBasedBuilderParametersImpl} object, and especially of the
    040 * {@link FileHandler} contained in this object. When the builder creates a new
    041 * object the resulting {@code Configuration} instance is associated with the
    042 * {@code FileHandler}. If the {@code FileHandler} has a location set, the
    043 * {@code Configuration} is directly loaded from this location.
    044 * </p>
    045 * <p>
    046 * The {@code FileHandler} is kept by this builder and can be queried later on.
    047 * It can be used for instance to save the current {@code Configuration} after
    048 * it was modified. Some care has to be taken when changing the location of the
    049 * {@code FileHandler}: The new location is recorded and also survives an
    050 * invocation of the {@code resetResult()} method. However, when the builder's
    051 * initialization parameters are reset by calling {@code resetParameters()} the
    052 * location is reset, too.
    053 * </p>
    054 *
    055 * @since 2.0
    056 * @param <T> the concrete type of {@code Configuration} objects created by this
    057 *        builder
    058 */
    059public class FileBasedConfigurationBuilder<T extends FileBasedConfiguration>
    060        extends BasicConfigurationBuilder<T>
    061{
    062    /** A map for storing default encodings for specific configuration classes. */
    063    private static final Map<Class<?>, String> DEFAULT_ENCODINGS =
    064            initializeDefaultEncodings();
    065
    066    /** Stores the FileHandler associated with the current configuration. */
    067    private FileHandler currentFileHandler;
    068
    069    /** A specialized listener for the auto save mechanism. */
    070    private AutoSaveListener autoSaveListener;
    071
    072    /** A flag whether the builder's parameters were reset. */
    073    private boolean resetParameters;
    074
    075    /**
    076     * Creates a new instance of {@code FileBasedConfigurationBuilder} which
    077     * produces result objects of the specified class.
    078     *
    079     * @param resCls the result class (must not be <b>null</b>
    080     * @throws IllegalArgumentException if the result class is <b>null</b>
    081     */
    082    public FileBasedConfigurationBuilder(final Class<? extends T> resCls)
    083    {
    084        super(resCls);
    085    }
    086
    087    /**
    088     * Creates a new instance of {@code FileBasedConfigurationBuilder} which
    089     * produces result objects of the specified class and sets initialization
    090     * parameters.
    091     *
    092     * @param resCls the result class (must not be <b>null</b>
    093     * @param params a map with initialization parameters
    094     * @throws IllegalArgumentException if the result class is <b>null</b>
    095     */
    096    public FileBasedConfigurationBuilder(final Class<? extends T> resCls,
    097            final Map<String, Object> params)
    098    {
    099        super(resCls, params);
    100    }
    101
    102    /**
    103     * Creates a new instance of {@code FileBasedConfigurationBuilder} which
    104     * produces result objects of the specified class and sets initialization
    105     * parameters and the <em>allowFailOnInit</em> flag.
    106     *
    107     * @param resCls the result class (must not be <b>null</b>
    108     * @param params a map with initialization parameters
    109     * @param allowFailOnInit the <em>allowFailOnInit</em> flag
    110     * @throws IllegalArgumentException if the result class is <b>null</b>
    111     */
    112    public FileBasedConfigurationBuilder(final Class<? extends T> resCls,
    113            final Map<String, Object> params, final boolean allowFailOnInit)
    114    {
    115        super(resCls, params, allowFailOnInit);
    116    }
    117
    118    /**
    119     * Returns the default encoding for the specified configuration class. If an
    120     * encoding has been set for the specified class (or one of its super
    121     * classes), it is returned. Otherwise, result is <b>null</b>.
    122     *
    123     * @param configClass the configuration class in question
    124     * @return the default encoding for this class (may be <b>null</b>)
    125     */
    126    public static String getDefaultEncoding(final Class<?> configClass)
    127    {
    128        String enc = DEFAULT_ENCODINGS.get(configClass);
    129        if (enc != null || configClass == null)
    130        {
    131            return enc;
    132        }
    133
    134        final List<Class<?>> superclasses =
    135                ClassUtils.getAllSuperclasses(configClass);
    136        for (final Class<?> cls : superclasses)
    137        {
    138            enc = DEFAULT_ENCODINGS.get(cls);
    139            if (enc != null)
    140            {
    141                return enc;
    142            }
    143        }
    144
    145        final List<Class<?>> interfaces = ClassUtils.getAllInterfaces(configClass);
    146        for (final Class<?> cls : interfaces)
    147        {
    148            enc = DEFAULT_ENCODINGS.get(cls);
    149            if (enc != null)
    150            {
    151                return enc;
    152            }
    153        }
    154
    155        return null;
    156    }
    157
    158    /**
    159     * Sets a default encoding for a specific configuration class. This encoding
    160     * is used if an instance of this configuration class is to be created and
    161     * no encoding has been set in the parameters object for this builder. The
    162     * encoding passed here not only applies to the specified class but also to
    163     * its sub classes. If the encoding is <b>null</b>, it is removed.
    164     *
    165     * @param configClass the name of the configuration class (must not be
    166     *        <b>null</b>)
    167     * @param encoding the default encoding for this class
    168     * @throws IllegalArgumentException if the class is <b>null</b>
    169     */
    170    public static void setDefaultEncoding(final Class<?> configClass, final String encoding)
    171    {
    172        if (configClass == null)
    173        {
    174            throw new IllegalArgumentException(
    175                    "Configuration class must not be null!");
    176        }
    177
    178        if (encoding == null)
    179        {
    180            DEFAULT_ENCODINGS.remove(configClass);
    181        }
    182        else
    183        {
    184            DEFAULT_ENCODINGS.put(configClass, encoding);
    185        }
    186    }
    187
    188    /**
    189     * {@inheritDoc} This method is overridden here to change the result type.
    190     */
    191    @Override
    192    public FileBasedConfigurationBuilder<T> configure(
    193            final BuilderParameters... params)
    194    {
    195        super.configure(params);
    196        return this;
    197    }
    198
    199    /**
    200     * Returns the {@code FileHandler} associated with this builder. If already
    201     * a result object has been created, this {@code FileHandler} can be used to
    202     * save it. Otherwise, the {@code FileHandler} from the initialization
    203     * parameters is returned (which is not associated with a {@code FileBased}
    204     * object). Result is never <b>null</b>.
    205     *
    206     * @return the {@code FileHandler} associated with this builder
    207     */
    208    public synchronized FileHandler getFileHandler()
    209    {
    210        return currentFileHandler != null ? currentFileHandler
    211                : fetchFileHandlerFromParameters();
    212    }
    213
    214    /**
    215     * {@inheritDoc} This implementation just records the fact that new
    216     * parameters have been set. This means that the next time a result object
    217     * is created, the {@code FileHandler} has to be initialized from
    218     * initialization parameters rather than reusing the existing one.
    219     */
    220    @Override
    221    public synchronized BasicConfigurationBuilder<T> setParameters(
    222            final Map<String, Object> params)
    223    {
    224        super.setParameters(params);
    225        resetParameters = true;
    226        return this;
    227    }
    228
    229    /**
    230     * Convenience method which saves the associated configuration. This method
    231     * expects that the managed configuration has already been created and that
    232     * a valid file location is available in the current {@code FileHandler}.
    233     * The file handler is then used to store the configuration.
    234     *
    235     * @throws ConfigurationException if an error occurs
    236     */
    237    public void save() throws ConfigurationException
    238    {
    239        getFileHandler().save();
    240    }
    241
    242    /**
    243     * Returns a flag whether auto save mode is currently active.
    244     *
    245     * @return <b>true</b> if auto save is enabled, <b>false</b> otherwise
    246     */
    247    public synchronized boolean isAutoSave()
    248    {
    249        return autoSaveListener != null;
    250    }
    251
    252    /**
    253     * Enables or disables auto save mode. If auto save mode is enabled, every
    254     * update of the managed configuration causes it to be saved automatically;
    255     * so changes are directly written to disk.
    256     *
    257     * @param enabled <b>true</b> if auto save mode is to be enabled,
    258     *        <b>false</b> otherwise
    259     */
    260    public synchronized void setAutoSave(final boolean enabled)
    261    {
    262        if (enabled)
    263        {
    264            installAutoSaveListener();
    265        }
    266        else
    267        {
    268            removeAutoSaveListener();
    269        }
    270    }
    271
    272    /**
    273     * {@inheritDoc} This implementation deals with the creation and
    274     * initialization of a {@code FileHandler} associated with the new result
    275     * object.
    276     */
    277    @Override
    278    protected void initResultInstance(final T obj) throws ConfigurationException
    279    {
    280        super.initResultInstance(obj);
    281        final FileHandler srcHandler =
    282                currentFileHandler != null && !resetParameters ? currentFileHandler
    283                        : fetchFileHandlerFromParameters();
    284        currentFileHandler = new FileHandler(obj, srcHandler);
    285
    286        if (autoSaveListener != null)
    287        {
    288            autoSaveListener.updateFileHandler(currentFileHandler);
    289        }
    290        initFileHandler(currentFileHandler);
    291        resetParameters = false;
    292    }
    293
    294    /**
    295     * Initializes the new current {@code FileHandler}. When a new result object
    296     * is created, a new {@code FileHandler} is created, too, and associated
    297     * with the result object. This new handler is passed to this method. If a
    298     * location is defined, the result object is loaded from this location.
    299     * Note: This method is called from a synchronized block.
    300     *
    301     * @param handler the new current {@code FileHandler}
    302     * @throws ConfigurationException if an error occurs
    303     */
    304    protected void initFileHandler(final FileHandler handler)
    305            throws ConfigurationException
    306    {
    307        initEncoding(handler);
    308        if (handler.isLocationDefined())
    309        {
    310            handler.locate();
    311            handler.load();
    312        }
    313    }
    314
    315    /**
    316     * Obtains the {@code FileHandler} from this builder's parameters. If no
    317     * {@code FileBasedBuilderParametersImpl} object is found in this builder's
    318     * parameters, a new one is created now and stored. This makes it possible
    319     * to change the location of the associated file even if no parameters
    320     * object was provided.
    321     *
    322     * @return the {@code FileHandler} from initialization parameters
    323     */
    324    private FileHandler fetchFileHandlerFromParameters()
    325    {
    326        FileBasedBuilderParametersImpl fileParams =
    327                FileBasedBuilderParametersImpl.fromParameters(getParameters(),
    328                        false);
    329        if (fileParams == null)
    330        {
    331            fileParams = new FileBasedBuilderParametersImpl();
    332            addParameters(fileParams.getParameters());
    333        }
    334        return fileParams.getFileHandler();
    335    }
    336
    337    /**
    338     * Installs the listener for the auto save mechanism if it is not yet
    339     * active.
    340     */
    341    private void installAutoSaveListener()
    342    {
    343        if (autoSaveListener == null)
    344        {
    345            autoSaveListener = new AutoSaveListener(this);
    346            addEventListener(ConfigurationEvent.ANY, autoSaveListener);
    347            autoSaveListener.updateFileHandler(getFileHandler());
    348        }
    349    }
    350
    351    /**
    352     * Removes the listener for the auto save mechanism if it is currently
    353     * active.
    354     */
    355    private void removeAutoSaveListener()
    356    {
    357        if (autoSaveListener != null)
    358        {
    359            removeEventListener(ConfigurationEvent.ANY, autoSaveListener);
    360            autoSaveListener.updateFileHandler(null);
    361            autoSaveListener = null;
    362        }
    363    }
    364
    365    /**
    366     * Initializes the encoding of the specified file handler. If already an
    367     * encoding is set, it is used. Otherwise, the default encoding for the
    368     * result configuration class is obtained and set.
    369     *
    370     * @param handler the handler to be initialized
    371     */
    372    private void initEncoding(final FileHandler handler)
    373    {
    374        if (StringUtils.isEmpty(handler.getEncoding()))
    375        {
    376            final String encoding = getDefaultEncoding(getResultClass());
    377            if (encoding != null)
    378            {
    379                handler.setEncoding(encoding);
    380            }
    381        }
    382    }
    383
    384    /**
    385     * Creates a map with default encodings for configuration classes and
    386     * populates it with default entries.
    387     *
    388     * @return the map with default encodings
    389     */
    390    private static Map<Class<?>, String> initializeDefaultEncodings()
    391    {
    392        final Map<Class<?>, String> enc = new ConcurrentHashMap<>();
    393        enc.put(PropertiesConfiguration.class,
    394                PropertiesConfiguration.DEFAULT_ENCODING);
    395        enc.put(XMLPropertiesConfiguration.class,
    396                XMLPropertiesConfiguration.DEFAULT_ENCODING);
    397        return enc;
    398    }
    399}