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 1790899 2017-04-10 21:56:46Z 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(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(Class<? extends T> resCls,
098            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(Class<? extends T> resCls,
114            Map<String, Object> params, 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(Class<?> configClass)
128    {
129        String enc = DEFAULT_ENCODINGS.get(configClass);
130        if (enc != null || configClass == null)
131        {
132            return enc;
133        }
134
135        List<Class<?>> superclasses =
136                ClassUtils.getAllSuperclasses(configClass);
137        for (Class<?> cls : superclasses)
138        {
139            enc = DEFAULT_ENCODINGS.get(cls);
140            if (enc != null)
141            {
142                return enc;
143            }
144        }
145
146        List<Class<?>> interfaces = ClassUtils.getAllInterfaces(configClass);
147        for (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(Class<?> configClass, 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            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            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(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(T obj) throws ConfigurationException
280    {
281        super.initResultInstance(obj);
282        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(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(FileHandler handler)
374    {
375        if (StringUtils.isEmpty(handler.getEncoding()))
376        {
377            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        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}