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}