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}