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