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