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