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}