/** * Copyright (c) 2005-2017, KoLmafia development team * http://kolmafia.sourceforge.net/ * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * [1] Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * [2] Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * [3] Neither the name "KoLmafia" nor the names of its contributors may * be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package net.sourceforge.kolmafia.preferences; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintStream; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; import net.java.dev.spellcast.utilities.DataUtilities; import net.sourceforge.kolmafia.KoLConstants; import net.sourceforge.kolmafia.combat.CombatActionManager; import net.sourceforge.kolmafia.listener.PreferenceListenerRegistry; import net.sourceforge.kolmafia.moods.MoodManager; import net.sourceforge.kolmafia.objectpool.IntegerPool; import net.sourceforge.kolmafia.persistence.HolidayDatabase; import net.sourceforge.kolmafia.session.ChoiceManager; import net.sourceforge.kolmafia.session.ChoiceManager.ChoiceAdventure; import net.sourceforge.kolmafia.swingui.AdventureFrame; import net.sourceforge.kolmafia.utilities.FileUtilities; import net.sourceforge.kolmafia.utilities.LogStream; import net.sourceforge.kolmafia.utilities.StringUtilities; import net.sourceforge.kolmafia.webui.CharPaneDecorator; public class Preferences { private static final byte [] LINE_BREAK_AS_BYTES = KoLConstants.LINE_BREAK.getBytes(); private static final String [] characterMap = new String[ 65536 ]; private static final HashMap<String, String> globalNames = new HashMap<String, String>(); private static final SortedMap<String, Object> globalValues = Collections.synchronizedSortedMap( new TreeMap<String, Object>() ); private static File globalPropertiesFile = null; private static final HashMap<String,String> userNames = new HashMap<String,String>(); private static final SortedMap<String, Object> userValues = Collections.synchronizedSortedMap( new TreeMap<String, Object>() ); private static File userPropertiesFile = null; private static final Set<String> defaultsSet = new HashSet<String>(); private static final Set<String> perUserGlobalSet = new HashSet<String>(); static { // Initialize perUserGlobalSet and read defaults.txt into // defaultsSet, globalNames, and userNames Preferences.initializeMaps(); // Read GLOBAL_prefs.txt into globalNames and globalValues Preferences.loadGlobalPreferences(); } private static final void initializeMaps() { // There are three specific per-user settings that appear in // GLOBAL_prefs.txt because the LoginFrame needs them Preferences.perUserGlobalSet.add( "saveState" ); Preferences.perUserGlobalSet.add( "displayName" ); Preferences.perUserGlobalSet.add( "getBreakfast" ); BufferedReader istream = FileUtilities.getVersionedReader( "defaults.txt", KoLConstants.DEFAULTS_VERSION ); String[] current; while ( ( current = FileUtilities.readData( istream ) ) != null ) { if ( current.length >= 2 ) { String map = current[ 0 ]; String name = current[ 1 ]; String value = current.length == 2 ? "" : current[ 2 ]; HashMap<String, String> desiredMap = map.equals( "global" ) ? Preferences.globalNames : Preferences.userNames; if ( desiredMap.containsKey( name ) ) { System.out.println( map + " setting " + name + " multiply defined" ); } HashMap<String, String> otherMap = map.equals( "global" ) ? Preferences.userNames : Preferences.globalNames; if ( otherMap != null && otherMap.containsKey( name ) ) { String other = map.equals( "global" ) ? "user" : "global"; System.out.println( map + " setting " + name + " already defined as a " + other + " setting" ); continue; } desiredMap.put( name, value ); // Maintain a set of prefs that exist in defaults.txt defaultsSet.add( name ); } } // Update Mac-specific properties values to ensure // that the displays are usable (by default). boolean isUsingMac = System.getProperty( "os.name" ).startsWith( "Mac" ); Preferences.globalNames.put( "useDecoratedTabs", String.valueOf( !isUsingMac ) ); Preferences.globalNames.put( "chatFontSize", isUsingMac ? "medium" : "small" ); try { istream.close(); } catch ( Exception e ) { // The stream is already closed, go ahead // and ignore this error. } } /** * Resets all settings so that the given user is represented whenever * settings are modified. */ public static synchronized final void reset( final String username ) { Preferences.saveToFile( Preferences.globalPropertiesFile, Preferences.globalValues ); // Prevent anybody from manipulating the user map until we are // done bulk-loading it. synchronized ( Preferences.userValues ) { if ( username == null || username.equals( "" ) ) { if ( Preferences.userPropertiesFile != null ) { Preferences.saveToFile( Preferences.userPropertiesFile, Preferences.userValues ); Preferences.userPropertiesFile = null; Preferences.userValues.clear(); } return; } Preferences.loadUserPreferences( username ); } AdventureFrame.updateFromPreferences(); CharPaneDecorator.updateFromPreferences(); CombatActionManager.updateFromPreferences(); MoodManager.updateFromPreferences(); PreferenceListenerRegistry.fireAllPreferencesChanged(); } public static final String baseUserName( final String name ) { return name == null || name.equals( "" ) ? "GLOBAL" : StringUtilities.globalStringReplace( name.trim(), " ", "_" ).toLowerCase(); } private static void loadGlobalPreferences() { File file = new File( KoLConstants.SETTINGS_LOCATION, Preferences.baseUserName( "" ) + "_prefs.txt" ); Preferences.globalPropertiesFile = file; Properties p = Preferences.loadPreferences( file ); Preferences.globalValues.clear(); // GLOBAL_prefs.txt can contain obsolete settings which // migrated from global to user. Leave them, since the // migration will pull the value from the global map for ( Entry<Object, Object> entry : p.entrySet() ) { String key = (String) entry.getKey(); if ( !Preferences.globalNames.containsKey( key ) && !Preferences.isPerUserGlobalProperty( key ) ) { // System.out.println( "obsolete global setting detected: " + key ); // continue; } String value = (String) entry.getValue(); Preferences.globalValues.put( key, value ); } // For all global properties in defaults.txt which were not in // GLOBAL_prefs.txt, add to global map with default value. for ( Entry<String,String>entry : Preferences.globalNames.entrySet() ) { String key = entry.getKey(); if ( !Preferences.globalValues.containsKey( key ) ) { // System.out.println( "Adding new built-in global setting: " + key ); String value = entry.getValue(); Preferences.globalValues.put( key, value ); } } } private static void loadUserPreferences( final String username ) { File file = new File( KoLConstants.SETTINGS_LOCATION, Preferences.baseUserName( username ) + "_prefs.txt" ); Preferences.userPropertiesFile = file; Properties p = Preferences.loadPreferences( file ); Preferences.userValues.clear(); for ( Entry<Object, Object> currentEntry : p.entrySet() ) { String key = (String) currentEntry.getKey(); String value = (String) currentEntry.getValue(); Preferences.userValues.put( key, value ); } for ( Entry<String,String>entry : Preferences.userNames.entrySet() ) { String key = entry.getKey(); if ( Preferences.userValues.containsKey( key ) ) { continue; } // If a user property in defaults.txt was not in // NAME_prefs.txt, add to user map with default value // (this is how we add a new user property) // // If it had a value in the GLOBAL map, use that (this // is how we migrate a preference from GLOBAL to user) String value = Preferences.globalValues.containsKey( key ) ? (String) Preferences.globalValues.get( key ) : (String) entry.getValue(); // System.out.println( "Adding new built-in user setting: " + key ); Preferences.userValues.put( key, value ); } } private static Properties loadPreferences( File file ) { InputStream istream = DataUtilities.getInputStream( file ); Properties p = new Properties(); try { p.load( istream ); } catch ( IOException e ) { System.out.println( e.getMessage() + " trying to load preferences from file." ); } try { istream.close(); } catch ( IOException e ) { System.out.println( e.getMessage() + " trying to close preferences file." ); } return p; } private static final String encodeProperty( String name, String value ) { StringBuffer buffer = new StringBuffer(); Preferences.encodeString( buffer, name ); if ( value != null && value.length() > 0 ) { buffer.append( "=" ); Preferences.encodeString( buffer, value ); } return buffer.toString(); } private static final void encodeString( StringBuffer buffer, String string ) { int length = string.length(); for ( int i = 0; i < length; ++i ) { char ch = string.charAt( i ); encodeCharacter( ch ); buffer.append( characterMap[ ch ] ); } } private static final void encodeCharacter( char ch ) { if ( characterMap[ ch ] != null ) { return; } switch ( ch ) { case '\t': characterMap[ ch ] = "\\t"; return; case '\n': characterMap[ ch ] = "\\n"; return; case '\f': characterMap[ ch ] = "\\f"; return; case '\r': characterMap[ ch ] = "\\r"; return; case '\\': case '=': case ':': case '#': case '!': characterMap[ ch ] = "\\" + ch; return; } characterMap[ ch ] = ( ch > 0x0019 && ch < 0x007f ) ? String.valueOf( ch ) : ( ch < 0x0010 ) ? "\\u000" + Integer.toHexString( ch ) : ( ch < 0x0100 ) ? "\\u00" + Integer.toHexString( ch ) : ( ch < 0x1000 ) ? "\\u0" + Integer.toHexString( ch ) : "\\u" + Integer.toHexString( ch ); } public static final boolean propertyExists( final String name, final boolean global ) { return global ? Preferences.globalValues.containsKey( name ) : Preferences.userValues.containsKey( name ); } public static final String getString( final String name, final boolean global ) { Object value = null; if ( global ) { if ( Preferences.globalValues.containsKey( name ) ) { value = Preferences.globalValues.get( name ); } } else { if ( Preferences.userValues.containsKey( name ) ) { value = Preferences.userValues.get( name ); } } return value == null ? "" : value.toString(); } public static final String getDefault( final String name ) { if ( Preferences.globalNames.containsKey( name ) ) { return Preferences.globalNames.get( name ); } if ( Preferences.userNames.containsKey( name ) ) { return Preferences.userNames.get( name ); } return ""; } public static final void removeProperty( final String name, final boolean global ) { // Remove only properties which do not have defaults if ( global ) { if ( !Preferences.globalNames.containsKey( name ) ) { // We are changing the structure of the map. // globalValues is a synchronized map. Preferences.globalValues.remove( name ); if ( Preferences.getBoolean( "saveSettingsOnSet" ) ) { Preferences.saveToFile( Preferences.globalPropertiesFile, Preferences.globalValues ); } } } else { if ( !Preferences.userNames.containsKey( name ) ) { // We are changing the structure of the map. // userValues is a synchronized map. Preferences.userValues.remove( name ); if ( Preferences.getBoolean( "saveSettingsOnSet" ) ) { Preferences.saveToFile( Preferences.userPropertiesFile, Preferences.userValues ); } } } } public static final boolean isGlobalProperty( final String name ) { return Preferences.globalNames.containsKey( name ); } public static boolean isPerUserGlobalProperty( final String property ) { if ( property.indexOf( "." ) != -1 ) { for ( String prefix : Preferences.perUserGlobalSet ) { if ( property.startsWith( prefix ) ) { return true; } } } return false; } public static final boolean isUserEditable( final String property ) { return !property.startsWith( "saveState" ) && !property.equals( "externalEditor" ) && !property.equals( "preferredWebBrowser" ); } public static final void setString( final String name, final String value ) { setString( null, name, value ); } public static final String getString( final String name ) { return getString( null, name ); } public static final void setBoolean( final String name, final boolean value ) { setBoolean( null, name, value ); } public static final boolean getBoolean( final String name ) { return getBoolean( null, name ); } public static final void setInteger( final String name, final int value ) { setInteger( null, name, value ); } public static final int getInteger( final String name ) { return getInteger( null, name ); } public static final void setFloat( final String name, final float value ) { setFloat( null, name, value ); } public static final void setLong( final String name, final long value ) { setLong( null, name, value ); } public static final long getLong( final String name ) { return getLong( null, name ); } public static final float getFloat( final String name ) { return getFloat( null, name ); } public static final int increment( final String name ) { return Preferences.increment( name, 1 ); } public static final int increment( final String name, final int delta ) { return Preferences.increment( name, delta, 0, false ); } public static final int increment( final String name, final int delta, final int max, final boolean mod ) { int current = Preferences.getInteger( name ); if ( delta != 0 ) { current += delta; if ( max > 0 && current >= max ) { if ( mod ) { current %= max; } else { current = max; } } Preferences.setInteger( name, current ); } return current; } public static final int decrement( final String name ) { return Preferences.decrement( name, 1 ); } public static final int decrement( final String name, final int delta ) { return Preferences.decrement( name, delta, 0 ); } public static final int decrement( final String name, final int delta, final int min ) { int current = Preferences.getInteger( name ); if ( delta != 0 ) { current -= delta; if ( current < min ) { current = min; } Preferences.setInteger( name, current ); } return current; } // Per-user global properties are stored in the global settings with // key "<name>.<user>" public static final String getString( final String user, final String name ) { Object value = Preferences.getObject( user, name ); if ( value == null ) { return ""; } return value.toString(); } public static final boolean getBoolean( final String user, final String name ) { Map<String, Object> map = Preferences.getMap( name ); Object value = Preferences.getObject( map, user, name ); if ( value == null ) { return false; } if ( !(value instanceof Boolean) ) { value = Boolean.valueOf( value.toString() ); map.put( name, value ); } return ((Boolean) value).booleanValue(); } public static final int getInteger( final String user, final String name ) { Map<String, Object> map = Preferences.getMap( name ); Object value = Preferences.getObject( map, user, name ); if ( value == null ) { return 0; } if ( !(value instanceof Integer) ) { value = IntegerPool.get( StringUtilities.parseInt( value.toString() ) ); map.put( name, value ); } return ((Integer) value).intValue(); } public static final long getLong( final String user, final String name ) { Map<String, Object> map = Preferences.getMap( name ); Object value = Preferences.getObject( map, user, name ); if ( value == null ) { return 0; } if ( !(value instanceof Long) ) { value = new Long( StringUtilities.parseLong( value.toString() ) ); map.put( name, value ); } return ((Long) value).longValue(); } public static final float getFloat( final String user, final String name ) { Map<String, Object> map = Preferences.getMap( name ); Object value = Preferences.getObject( map, user, name ); if ( value == null ) { return 0.0f; } if ( !(value instanceof Float) ) { value = new Float( StringUtilities.parseFloat( value.toString() ) ); map.put( name, value ); } return ((Float) value).floatValue(); } private static final Map<String, Object> getMap( final String name ) { return Preferences.isGlobalProperty( name ) ? Preferences.globalValues : Preferences.userValues; } private static final Object getObject( final String user, final String name ) { return Preferences.getObject( Preferences.getMap( name ), user, name ); } private static final Object getObject( final Map<String, Object> map, final String user, final String name ) { String key = Preferences.propertyName( user, name ); return map.get( key ); } public static final TreeMap<String, String> getMap( boolean defaults, boolean user ) { if ( defaults ) { return new TreeMap<String, String>( user ? userNames : globalNames ); } else { TreeMap<String, String> map = new TreeMap<String, String>(); Map<String, Object> srcmap = user ? userValues : globalValues; for ( String pref : srcmap.keySet() ) { map.put( pref, getString( pref ) ); } return map; } } public static final void setString( final String user, final String name, final String value ) { String old = Preferences.getString( user, name ); if ( !old.equals( value ) ) { Preferences.setObject( user, name, value, value ); } } public static final void setBoolean( final String user, final String name, final boolean value ) { boolean old = Preferences.getBoolean( user, name ); if ( old != value ) { Preferences.setObject( user, name, value ? "true" : "false", Boolean.valueOf( value ) ); } } public static final void setInteger( final String user, final String name, final int value ) { int old = Preferences.getInteger( user, name ); if ( old != value ) { Preferences.setObject( user, name, String.valueOf( value ), IntegerPool.get( value ) ); } } public static final void setLong( final String user, final String name, final long value ) { long old = Preferences.getLong( user, name ); if ( old != value ) { Preferences.setObject( user, name, String.valueOf( value ), new Long( value ) ); } } public static final void setFloat( final String user, final String name, final float value ) { float old = Preferences.getFloat( user, name ); if ( old != value ) { Preferences.setObject( user, name, String.valueOf( value ), new Float( value ) ); } } private static final void setObject( final String user, final String name, final String value, final Object object ) { if ( Preferences.isGlobalProperty( name ) ) { String actualName = Preferences.propertyName( user, name ); // We might be changing the structure of the map. // globalValues is a synchronized map. Preferences.globalValues.put( actualName, object ); if ( Preferences.getBoolean( "saveSettingsOnSet" ) ) { Preferences.saveToFile( Preferences.globalPropertiesFile, Preferences.globalValues ); } } else if ( Preferences.userPropertiesFile != null ) { // We might be changing the structure of the map. // userValues is a synchronized map. Preferences.userValues.put( name, object ); if ( Preferences.getBoolean( "saveSettingsOnSet" ) ) { Preferences.saveToFile( Preferences.userPropertiesFile, Preferences.userValues ); } } PreferenceListenerRegistry.firePreferenceChanged( name ); if ( name.startsWith( "choiceAdventure" ) ) { PreferenceListenerRegistry.firePreferenceChanged( "choiceAdventure*" ); } } private static final String propertyName( final String user, final String name ) { return user == null ? name : name + "." + Preferences.baseUserName( user ); } private static final void saveToFile( File file, Map<String, Object> data ) { // See Collections.synchronizedSortedMap // // We are essentially iterating over the map. Not exactly - we // are iterating over the entrySet - but let's keep the map and // the file in synch atomically synchronized ( data ) { // Determine the contents of the file by // actually printing them. ByteArrayOutputStream ostream = new ByteArrayOutputStream(); try { for ( Entry<String, Object> current : data.entrySet() ) { ostream.write( Preferences.encodeProperty( current.getKey(), current.getValue().toString() ).getBytes() ); ostream.write( LINE_BREAK_AS_BYTES ); } } catch ( IOException e ) { System.out.println(e.getMessage()+" trying to write preferences as byte array."); } OutputStream fstream = DataUtilities.getOutputStream( file ); try { ostream.writeTo( fstream ); } catch ( IOException e ) { System.out.println(e.getMessage()+" trying to write preferences as stream."); } try { fstream.close(); } catch ( IOException e ) { System.out.println(e.getMessage()+" trying to close preferences stream."); } } } public static final void printDefaults() { PrintStream ostream = LogStream.openStream( "choices.txt", true ); ostream.println( "[u]Configurable[/u]" ); ostream.println(); ChoiceManager.setChoiceOrdering( false ); Arrays.sort( ChoiceManager.CHOICE_ADVS ); Arrays.sort( ChoiceManager.CHOICE_ADV_SPOILERS ); Preferences.printDefaults( ChoiceManager.CHOICE_ADVS, ostream ); ostream.println(); ostream.println(); ostream.println( "[u]Not Configurable[/u]" ); ostream.println(); Preferences.printDefaults( ChoiceManager.CHOICE_ADV_SPOILERS, ostream ); ChoiceManager.setChoiceOrdering( true ); Arrays.sort( ChoiceManager.CHOICE_ADVS ); Arrays.sort( ChoiceManager.CHOICE_ADV_SPOILERS ); ostream.close(); } private static final void printDefaults( final ChoiceAdventure[] choices, final PrintStream ostream ) { for ( int i = 0; i < choices.length; ++i ) { String setting = choices[ i ].getSetting(); ostream.print( "[" + setting.substring( 15 ) + "] " ); ostream.print( choices[ i ].getName() + ": " ); Object[] options = choices[ i ].getOptions(); int defaultOption = StringUtilities.parseInt( Preferences.userNames.get( setting ) ); Object def = ChoiceManager.findOption( options, defaultOption ); ostream.print( def.toString() + " [color=gray](" ); int printedCount = 0; for ( int j = 0; j < options.length; ++j ) { Object option = options[ j ]; if ( option == def ) { continue; } if ( printedCount != 0 ) { ostream.print( ", " ); } ++printedCount; ostream.print( option.toString() ); } ostream.println( ")[/color]" ); } } public static void resetToDefault( String name ) { if ( Preferences.userNames.containsKey( name ) ) { Preferences.setString( name, Preferences.userNames.get( name ) ); } else if ( Preferences.globalNames.containsKey( name ) ) { Preferences.setString( name, Preferences.globalNames.get( name ) ); } } public static void resetDailies() { // See Collections.synchronizedSortedMap // // userValues is a synchronized map, but we are doing a mass // change to it. synchronized ( Preferences.userValues ) { Iterator<String> it = Preferences.userValues.keySet().iterator(); while ( it.hasNext() ) { String name = it.next(); if ( name.startsWith( "_" ) ) { if ( !Preferences.containsDefault( name ) ) { // fully delete preferences that start with _ and aren't in defaults.txt it.remove(); continue; } String val = Preferences.userNames.get( name ); if ( val == null ) val = ""; Preferences.setString( name, val ); } } if ( Preferences.getBoolean( "saveSettingsOnSet" )) { Preferences.saveToFile( Preferences.userPropertiesFile, Preferences.userValues ); } } } public static void resetGlobalDailies() { // See Collections.synchronizedSortedMap // // globalValues is a synchronized map, but we are doing a mass // change to it. synchronized ( Preferences.globalValues ) { for ( String name : Preferences.globalValues.keySet() ) { if ( name.startsWith( "_" ) ) { String val = Preferences.globalNames.get( name ); if ( val == null ) val = ""; Preferences.setString( name, val ); } } Preferences.setInteger( "lastGlobalCounterDay", HolidayDatabase.getPhaseStep() ); if ( Preferences.getBoolean( "saveSettingsOnSet" )) { Preferences.saveToFile( Preferences.globalPropertiesFile, Preferences.globalValues ); } } } public static boolean containsDefault( String key ) { return defaultsSet.contains( key ); } }