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