package org.limewire.setting; import java.awt.Color; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Properties; import java.util.Set; import org.limewire.util.FileUtils; /** * Coordinates the creating, storing and reloading of persistent data to and * from disk for {@link AbstractSetting} objects. Each <code>Setting</code> creation * method takes the name of the key and the default value, and all settings * are typed. Since duplicate keys aren't allowed, you must choose a unique * string for your setting key name, otherwise an exception, * <code>IllegalArgumentException</code> is thrown. * <p> * When you add a new <code>Setting</code> subclass, add a public synchronized * method to <code>SettingsFactory</code> to create an instance of the setting. * For example, subclass {@link IntSetting}, <code>SettingsFactory</code> has * {@link #createIntSetting(String, int)} and * {@link #createRemoteIntSetting(String, int, String, int, int)}. * <p> * An example of creating an {@link IntSetting} that uses setting.txt, without the key * MAX_MESSAGE_SIZE previously included: <pre> File f = new File("setting.txt"); SettingsFactory sf = new SettingsFactory(f); IntSetting intsetting = sf.createIntSetting("MAX_MESSAGE_SIZE", 1492); System.out.println("1: " + intsetting.getValue()); intsetting.setValue("2984"); System.out.println("2: " + intsetting.getValue()); sf.save(); Output: 1: 1492 2: 2984 </pre> With the call sf.save(), setting.txt now includes: <pre> MAX_MESSAGE_SIZE=2984 </pre> * Additionally, the value stored in disk is loaded for each key * you specify regardless of the default value in the create method. For example * with "MAX_MESSAGE_SIZE=2984" stored in setting.txt: <pre> File f = new File("setting.txt"); SettingsFactory sf = new SettingsFactory(f); IntSetting intsetting = sf.createIntSetting("MAX_MESSAGE_SIZE", 0); System.out.println(intsetting.getValue()); sf.save(); Output: 2984 </pre> font.txt still includes: <pre> MAX_MESSAGE_SIZE=2984 </pre> * If setting.txt didn't have the key MAX_MESSAGE_SIZE prior to the * <code>createIntSetting</code> call, then the MAX_MESSAGE_SIZE is 0. */ public final class SettingsFactory implements Iterable<AbstractSetting>, RemoteSettingController { /** Marked true in the event of an error in the load/save of any settings file. */ private static boolean loadSaveFailureEncountered = false; /** Time interval, after which the accumulated information expires. */ private static final long EXPIRY_INTERVAL = 14 * 24 * 60 * 60 * 1000; //14 days /** An internal Setting to store the last expire time. */ private LongSetting LAST_EXPIRE_TIME = null; /** An internal Setting that controls whether or not unlisted remote settings revert to default. */ private BooleanSetting REVERT_UNLISTED_REMOTE = null; /** <tt>File</tt> object from which settings are loaded and saved. */ private File SETTINGS_FILE; /** The header written to the settings file. */ private final String HEADING; /** <tt>Properties</tt> instance for the default values. */ protected final Properties DEFAULT_PROPS = new Properties(); /** The <tt>Properties</tt> instance containing all settings. */ protected final Properties PROPS = new Properties(DEFAULT_PROPS); /** * List of all settings associated with this factory * LOCKING: must hold this monitor */ private ArrayList<AbstractSetting> settings = new ArrayList<AbstractSetting>(10); /** * A mapping of remoteKeys to Settings. Only remote Enabled settings will be * added to this list. As setting are created, they are added to this map so * that when remote settings are loaded, it's easy to find the targeted * settings. */ private Map<String, AbstractSetting> remoteKeyToSetting = new HashMap<String, AbstractSetting>(); /** * The RemoteSettingsManager being used to control remote settings. */ private RemoteSettingManager remoteManager = new NullRemoteManager(); /** Whether or not expirable settings have expired. */ private boolean expired = false; /** * Creates a new <tt>SettingsFactory</tt> instance with the specified file * to read from and write to. * * @param settingsFile the file to read from and to write to */ public SettingsFactory(File settingsFile) { this(settingsFile, ""); } /** * Creates a new <tt>SettingsFactory</tt> instance with the specified file * to read from and write to. * * @param settingsFile the file to read from and to write to * @param heading heading to use when writing property file */ public SettingsFactory(File settingsFile, String heading) { SETTINGS_FILE = settingsFile; if (SETTINGS_FILE.isDirectory()) SETTINGS_FILE.delete(); HEADING = heading; reload(); } /** * Indicated if a failure has occurred for delayed reporting */ public static boolean hasLoadSaveFailure() { return loadSaveFailureEncountered; } /** * Saves a failure event for delayed reporting. */ private static void markFailure() { loadSaveFailureEncountered = true; } /** * Resets the failure flag. */ public static void resetLoadSaveFailure() { loadSaveFailureEncountered = false; } /** * Returns the iterator over the settings stored in this factory. * <p> * LOCKING: The caller must ensure that this factory's monitor * is held while iterating over the iterator. */ public synchronized Iterator<AbstractSetting> iterator() { return settings.iterator(); } /** * Returns the setting that controls whether or not remote settings * are reverted when loaded. */ public BooleanSetting getRevertSetting() { return REVERT_UNLISTED_REMOTE; } /** * Reloads the settings with the predefined settings file from * disk. */ public synchronized void reload() { // Setup the key that tells us whether or not to revert // remote settings that are not listed in the remote updates. if(REVERT_UNLISTED_REMOTE == null) REVERT_UNLISTED_REMOTE = createBooleanSetting("REVERT_UNLISTED_REMOTE", true); // If the props file doesn't exist, the init sequence will prompt // the user for the required values, so return. If this is not // loading limewire.props, but rather something like themes.txt, // we also return, as attempting to load an invalid file will // not do any good. if(!SETTINGS_FILE.isFile()) { setExpireValue(); return; } FileInputStream fis = null; try { fis = new FileInputStream(SETTINGS_FILE); try { PROPS.load(fis); } catch(IllegalArgumentException e) { // Ignored -- Use best guess } catch(StringIndexOutOfBoundsException e) { // Ignored -- Use best guess } catch(IOException e) { // Serious Problems --- Use defaults markFailure(); } } catch(FileNotFoundException e) { if (SETTINGS_FILE.exists()) { markFailure(); } } finally { FileUtils.close(fis); } // Reload all setting values for(Setting set : settings) set.reload(); setExpireValue(); } /** * Sets the last expire time if not already set. */ private synchronized void setExpireValue() { // Note: this has only an impact on launch time when this // method is called by the constructor of this class! if (LAST_EXPIRE_TIME == null) { LAST_EXPIRE_TIME = createLongSetting("LAST_EXPIRE_TIME", 0); // Set flag to true if Settings are expired. See // createExpirable<whatever>Setting at the bottom expired = (LAST_EXPIRE_TIME.getValue() + EXPIRY_INTERVAL < System.currentTimeMillis()); if (expired) LAST_EXPIRE_TIME.setValue(System.currentTimeMillis()); } } /** * Changes the backing file to use for this factory. */ public synchronized void changeFile(File toUse) { SETTINGS_FILE = toUse; if (SETTINGS_FILE.isDirectory()) SETTINGS_FILE.delete(); revertToDefault(); reload(); } /** * Reverts all settings to their factory defaults. */ public synchronized boolean revertToDefault() { boolean any = false; for(Setting setting : settings) { any |= setting.revertToDefault(); } return any; } /** * Save setting information to property file * We want to NOT save any properties which are the default value, * as well as any older properties that are no longer in use. * To avoid having to manually encode the file, we clone * the existing properties and manually remove the ones * which are default and aren't required to be saved. * It is important to do it this way (as opposed to creating a new * properties object and adding only those that should be saved * or aren't default) because 'adding' properties may fail if * certain settings classes haven't been statically loaded yet. * (Note that we cannot use 'store' since it's only available in 1.2.) */ public synchronized void save() { Properties toSave = (Properties) PROPS.clone(); //Add any settings which require saving or aren't default for(Setting set : settings) { if( !set.shouldAlwaysSave() && set.isDefault() ) toSave.remove( set.getKey() ); } OutputStream out = null; try { // some bugs were reported where the settings file was a directory. if (SETTINGS_FILE.isDirectory()) SETTINGS_FILE.delete(); // some bugs were reported where the settings file's parent // directory was deleted. File parent = SETTINGS_FILE.getParentFile(); if(parent != null) { parent.mkdirs(); } FileUtils.setWriteable(SETTINGS_FILE); if (SETTINGS_FILE.exists() && !SETTINGS_FILE.canRead()) { SETTINGS_FILE.delete(); } try { out = new BufferedOutputStream(new FileOutputStream(SETTINGS_FILE)); } catch(IOException ioe) { // Try again. if (SETTINGS_FILE.exists()) { SETTINGS_FILE.delete(); out = new BufferedOutputStream(new FileOutputStream(SETTINGS_FILE)); } } if(out != null) { // save the properties to disk. toSave.store(out, HEADING); } else { markFailure(); } } catch (IOException e) { markFailure(); } finally { FileUtils.close(out); } } @Override public String toString() { return PROPS.toString(); } /** * Return settings properties. */ Properties getProperties() { return PROPS; } /** Sets a new RemoteSettingManager to control remote settings. * */ public synchronized void setRemoteSettingManager(RemoteSettingManager manager) { this.remoteManager = manager; manager.setRemoteSettingController(this); } public synchronized boolean updateSetting(String remoteKey, String value) { AbstractSetting setting = remoteKeyToSetting.get(remoteKey); if(setting != null) { setting.setValueInternal(value); return true; } else { return false; } } /** * If we're reverting unlisted remote settings, then go through every setting * we know of that's considered remote and revert it, if it isn't in the list * of current remotely set settings. */ public synchronized void revertRemoteSettingsUnlessIn(Set<String> keySet) { if(REVERT_UNLISTED_REMOTE.getValue()) { for(Map.Entry<String, AbstractSetting> entry : remoteKeyToSetting.entrySet()) { if(!keySet.contains(entry.getKey())) { entry.getValue().revertToDefault(); } } } } /** * Creates a new <tt>StringSetting</tt> instance with the specified * key and default value. * * @param key the key for the setting * @param defaultValue the default value for the setting, cannot be null */ public synchronized StringSetting createStringSetting(String key, String defaultValue) { StringSetting result = new StringSetting(DEFAULT_PROPS, PROPS, key, defaultValue); handleSettingInternal(result, null); return result; } public synchronized StringSetting createRemoteStringSetting(String key, String defaultValue, String remoteKey) { StringSetting result = new StringSetting(DEFAULT_PROPS, PROPS, key, defaultValue); handleSettingInternal(result, remoteKey); return result; } /** * Creates a new <tt>BooleanSetting</tt> instance with the specified * key and default value. * * @param key the key for the setting * @param defaultValue the default value for the setting */ public synchronized BooleanSetting createBooleanSetting(String key, boolean defaultValue) { BooleanSetting result = new BooleanSettingImpl(DEFAULT_PROPS, PROPS, key, defaultValue); handleSettingInternal((AbstractSetting)result, null); return result; } /** * If max != min, the setting becomes unsettable. */ public synchronized BooleanSetting createRemoteBooleanSetting(String key, boolean defaultValue, String remoteKey) { BooleanSetting result = new BooleanSettingImpl(DEFAULT_PROPS, PROPS, key, defaultValue); handleSettingInternal((AbstractSetting)result, remoteKey); return result; } /** * Creates a new <tt>IntSetting</tt> instance with the specified * key and default value. * * @param key the key for the setting * @param defaultValue the default value for the setting */ public synchronized IntSetting createIntSetting(String key, int defaultValue) { IntSetting result = new IntSetting(DEFAULT_PROPS, PROPS, key, defaultValue); handleSettingInternal(result, null); return result; } /** * Creates a new <tt>IntSetting</tt> instance with the specified * key and default value. * * @param key the key for the setting * @param defaultValue the default value for the setting * @param min the minimum value for this setting * @param the maximum value for this setting */ public synchronized IntSetting createIntSetting(String key, int defaultValue, int min, int max) { IntSetting result = new IntSetting(DEFAULT_PROPS, PROPS, key, defaultValue, min, max); handleSettingInternal(result, null); return result; } /** * Creates a new <tt>IntSetting</tt> instance with the specified * key and default value. * * @param key the key for the setting * @param defaultValue the default value for the setting */ public synchronized IntSetting createRemoteIntSetting(String key, int defaultValue, String remoteKey, int min, int max) { IntSetting result = new IntSetting(DEFAULT_PROPS, PROPS, key, defaultValue, min, max); handleSettingInternal(result, remoteKey); return result; } /** * Creates a new <tt>ByteSetting</tt> instance with the specified * key and default value. * * @param key the key for the setting * @param defaultValue the default value for the setting */ public synchronized ByteSetting createByteSetting(String key, byte defaultValue) { ByteSetting result = new ByteSetting(DEFAULT_PROPS, PROPS, key, defaultValue); handleSettingInternal(result, null); return result; } public synchronized ByteSetting createRemoteByteSetting(String key, byte defaultValue, String remoteKey, byte min, byte max) { ByteSetting result = new ByteSetting(DEFAULT_PROPS, PROPS, key, defaultValue, min, max); handleSettingInternal(result, remoteKey); return result; } /** * Creates a new <tt>LongSetting</tt> instance with the specified * key and default value. * * @param key the key for the setting * @param defaultValue the default value for the setting */ public synchronized LongSetting createLongSetting(String key, long defaultValue) { LongSetting result = new LongSetting(DEFAULT_PROPS, PROPS, key, defaultValue); handleSettingInternal(result, null); return result; } public synchronized LongSetting createRemoteLongSetting(String key, long defaultValue, String remoteKey, long min, long max) { LongSetting result = new LongSetting(DEFAULT_PROPS, PROPS, key, defaultValue, min, max); handleSettingInternal(result, remoteKey); return result; } /** * Creates a new <tt>PowerOfTwoSetting</tt> instance with the specified * key and default value. * * @param key the key for the setting * @param defaultValue the default value for the setting, which must be a * power of two. */ public synchronized PowerOfTwoSetting createPowerOfTwoSetting(String key, long defaultValue) { PowerOfTwoSetting result = new PowerOfTwoSetting(DEFAULT_PROPS, PROPS, key, defaultValue); handleSettingInternal(result, null); return result; } public synchronized PowerOfTwoSetting createRemotePowerOfTwoSetting(String key, long defaultValue, String remoteKey, long min, long max) { PowerOfTwoSetting result = new PowerOfTwoSetting(DEFAULT_PROPS, PROPS, key, defaultValue, min, max); handleSettingInternal(result, remoteKey); return result; } /** * Creates a new <tt>FileSetting</tt> instance with the specified * key and default value. * * @param key the key for the setting * @param defaultValue the default value for the setting */ public synchronized FileSetting createFileSetting(String key, File defaultValue) { // Creation of parent dirs removed per LWC-1323. FileSetting result = new FileSetting(DEFAULT_PROPS, PROPS, key, defaultValue); handleSettingInternal(result, null); return result; } public synchronized FileSetting createRemoteFileSetting(String key, File defaultValue, String remoteKey) { String parentString = defaultValue.getParent(); if( parentString != null ) { File parent = new File(parentString); if(!parent.isDirectory()) parent.mkdirs(); } FileSetting result = new FileSetting( DEFAULT_PROPS, PROPS, key, defaultValue); handleSettingInternal(result, remoteKey); return result; } public synchronized ProxyFileSetting createProxyFileSetting(String key, FileSetting defaultSetting) { ProxyFileSetting result = new ProxyFileSetting(DEFAULT_PROPS, PROPS, key, defaultSetting); handleSettingInternal(result, null); return result; } /** * Creates a new <tt>ColorSetting</tt> instance with the specified * key and default value. * * @param key the key for the setting * @param defaultValue the default value for the setting */ public synchronized ColorSetting createColorSetting(String key, Color defaultValue) { ColorSetting result = ColorSetting.createColorSetting(DEFAULT_PROPS, PROPS, key,defaultValue); handleSettingInternal(result, null); return result; } public synchronized ColorSetting createRemoteColorSetting(String key, Color defaultValue, String remoteKey) { ColorSetting result = ColorSetting.createColorSetting(DEFAULT_PROPS, PROPS, key, defaultValue); handleSettingInternal(result, remoteKey); return result; } /** * Creates a new <tt>CharArraySetting</tt> instance for a character array * setting with the specified key and default value. * * @param key the key for the setting * @param defaultValue the default value for the setting */ public synchronized CharArraySetting createCharArraySetting(String key, char[] defaultValue) { CharArraySetting result = new CharArraySetting(DEFAULT_PROPS, PROPS, key, defaultValue); handleSettingInternal(result, null); return result; } public synchronized CharArraySetting createRemoteCharArraySetting( String key, char[] defaultValue, String remoteKey) { CharArraySetting result =new CharArraySetting(DEFAULT_PROPS, PROPS, key, defaultValue); handleSettingInternal(result, remoteKey); return result; } /** * Creates a new <tt>FloatSetting</tt> instance with the specified * key and default value. * * @param key the key for the setting * @param defaultValue the default value for the setting */ public synchronized FloatSetting createFloatSetting(String key, float defaultValue) { FloatSetting result = new FloatSetting(DEFAULT_PROPS, PROPS, key, defaultValue); handleSettingInternal(result, null); return result; } public synchronized FloatSetting createFloatSetting(String key, float defaultValue, float min, float max) { FloatSetting result = new FloatSetting(DEFAULT_PROPS, PROPS, key, defaultValue, min, max); handleSettingInternal(result, null); return result; } public synchronized FloatSetting createRemoteFloatSetting(String key, float defaultValue, String remoteKey, float min, float max) { FloatSetting result = new FloatSetting(DEFAULT_PROPS, PROPS, key, defaultValue, min, max); handleSettingInternal(result, remoteKey); return result; } /** * Creates a new <tt>StringArraySetting</tt> instance for a String array * setting with the specified key and default value. * * @param key the key for the setting * @param defaultValue the default value for the setting */ public synchronized StringArraySetting createStringArraySetting(String key, String[] defaultValue) { StringArraySetting result = new StringArraySetting(DEFAULT_PROPS, PROPS, key, defaultValue); handleSettingInternal(result, null); return result; } /** * Creates a new <tt>PropertiesSetting</tt> instance for a Properties * setting with the specified key and default value. * * @param key the key for the setting * @param defaultValue the default value for the setting */ public synchronized PropertiesSetting createPropertiesSetting(String key, Properties defaultValue) { PropertiesSetting result = new PropertiesSetting(DEFAULT_PROPS, PROPS, key, defaultValue); handleSettingInternal(result, null); return result; } public synchronized StringArraySetting createRemoteStringArraySetting( String key, String[] defaultValue, String remoteKey) { StringArraySetting result = new StringArraySetting(DEFAULT_PROPS, PROPS, key, defaultValue); handleSettingInternal(result, remoteKey); return result; } public synchronized StringSetSetting createStringSetSetting(String key, String defaultValue) { StringSetSetting result = new StringSetSetting(DEFAULT_PROPS, PROPS, key, defaultValue); handleSettingInternal(result, null); return result; } /** * Creates a new <tt>FileArraySetting</tt> instance for a File array * setting with the specified key and default value. * * @param key the key for the setting * @param defaultValue the default value for the setting */ public synchronized FileArraySetting createFileArraySetting(String key, File[] defaultValue) { FileArraySetting result = new FileArraySetting(DEFAULT_PROPS, PROPS, key, defaultValue); handleSettingInternal(result, null); return result; } public synchronized FileArraySetting createRemoteFileArraySetting( String key, File[] defaultValue, String remoteKey) { FileArraySetting result = new FileArraySetting(DEFAULT_PROPS, PROPS, key, defaultValue); handleSettingInternal(result, remoteKey); return result; } /** * Creates a new <tt>FileSetSetting</tt> instance for a File array * setting with the specified key and default value. * * @param key the key for the setting * @param defaultValue the default value for the setting */ public synchronized FileSetSetting createFileSetSetting(String key, File[] defaultValue) { FileSetSetting result = new FileSetSetting(DEFAULT_PROPS, PROPS, key, defaultValue); handleSettingInternal(result, null); return result; } public synchronized FileSetSetting createRemoteFileSetSetting( String key, File[] defaultValue, String remoteKey) { FileSetSetting result = new FileSetSetting(DEFAULT_PROPS, PROPS, key, defaultValue); handleSettingInternal(result, remoteKey); return result; } /** * Creates a new expiring <tt>BooleanSetting</tt> instance with the * specified key and default value. * * @param key the key for the setting * @param defaultValue the default value for the setting */ public synchronized BooleanSetting createExpirableBooleanSetting(String key, boolean defaultValue) { BooleanSetting result = createBooleanSetting(key, defaultValue); if (expired) result.revertToDefault(); return result; } /** * Creates a new expiring <tt>IntSetting</tt> instance with the specified * key and default value. * * @param key the key for the setting * @param defaultValue the default value for the setting */ public synchronized IntSetting createExpirableIntSetting(String key, int defaultValue) { IntSetting result = createIntSetting(key, defaultValue); if (expired) result.revertToDefault(); return result; } /** * Creates a new expiring <tt>LongSetting</tt> instance with the specified * key and default value. * * @param key the key for the setting * @param defaultValue the default value for the setting */ public synchronized LongSetting createExpirableLongSetting(String key, long defaultValue) { LongSetting result = createLongSetting(key, defaultValue); if (expired) result.revertToDefault(); return result; } /** * Creates a new <tt>FontNameSetting</tt> instance with the specified * key and default value. * * @param key the key for the setting * @param defaultValue the default value for the setting */ public synchronized FontNameSetting createFontNameSetting(String key, String defaultValue){ FontNameSetting result = new FontNameSetting(DEFAULT_PROPS, PROPS, key, defaultValue); handleSettingInternal(result, null); return result; } public synchronized FontNameSetting createRemoteFontNameSetting( String key, String defaultValue, String remoteKey) { FontNameSetting result = new FontNameSetting(DEFAULT_PROPS, PROPS, key, defaultValue); handleSettingInternal(result, remoteKey); return result; } public synchronized ProbabilisticBooleanSetting createProbabilisticBooleanSetting( String key, float defaultValue) { ProbabilisticBooleanSetting result = new ProbabilisticBooleanSetting(DEFAULT_PROPS, PROPS, key, defaultValue); handleSettingInternal(result, null); return result; } public synchronized ProbabilisticBooleanSetting createRemoteProbabilisticBooleanSetting( String key, float defaultValue, String remoteKey, float min, float max) { ProbabilisticBooleanSetting result = new ProbabilisticBooleanSetting(DEFAULT_PROPS, PROPS, key, defaultValue, min, max); handleSettingInternal(result, remoteKey); return result; } /** * Creates a new <tt>PasswordSetting</tt> instance with the specified * key and default value. * * @param key the key for the setting * @param defaultValue the default value for the setting */ public synchronized PasswordSetting createPasswordSettingMD5( String key, String defaultValue) { PasswordSetting result = new PasswordSetting(DEFAULT_PROPS, PROPS, PasswordSetting.MD5, key, defaultValue); handleSettingInternal(result, null); return result; } private synchronized void handleSettingInternal(AbstractSetting setting, String remoteKey) { settings.add(setting); setting.reload(); //remote related checks... if(remoteKey != null) { if (remoteKeyToSetting.containsKey(remoteKey)) { throw new IllegalArgumentException("duplicate setting remoteKey: " + remoteKey); } String remoteValue = remoteManager.getUnloadedValueFor(remoteKey); if(remoteValue != null) { setting.setValueInternal(remoteValue); } else if(REVERT_UNLISTED_REMOTE.getValue()) { // As we load this setting, if it's not in the remote settings, // revert it. It's OK if we revert settings that are later added // to a remote setting, because it'll update the value. setting.revertToDefault(); } //update the mapping of the remote key to the setting. remoteKeyToSetting.put(remoteKey, setting); } } }