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