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}