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}