/* * Project Scelight * * Copyright (c) 2013 Andras Belicza <[email protected]> * * This software is the property of Andras Belicza. * Copying, modifying, distributing, refactoring without the author's permission * is prohibited and protected by Law. */ package hu.sllauncher.bean.settings; import hu.scelightapibase.bean.settings.ISettingChangeListener; import hu.scelightapibase.bean.settings.ISettingsBean; import hu.scelightapibase.bean.settings.type.INodeSetting; import hu.scelightapibase.bean.settings.type.ISetting; import hu.sllauncher.bean.Bean; import hu.sllauncher.bean.VersionBean; import hu.sllauncher.bean.settings.adapter.SettingsMapAdapter; import hu.sllauncher.service.env.LEnv; import java.lang.reflect.Field; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.WeakHashMap; import javax.xml.bind.JAXB; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlTransient; import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; /** * Settings bean implementation. * * <p> * Implements change tracking which is turned off by default. * </p> * * <p> * Also implements registering and notifying setting change listeners. * </p> * * @author Andras Belicza */ @XmlAccessorType( XmlAccessType.FIELD ) public class SettingsBean extends Bean implements ISettingsBean { /** * Utility method which creates a list of settings from the fields of the specified class which stores a collection of settings as public static class * fields. * * @param settingCollectionClass class holding settings as public static fields * @return a list of settings gained from the specified class */ public static List< ISetting< ? > > createSettingListFromFields( final Class< ? > settingCollectionClass ) { final List< ISetting< ? > > settingList = new ArrayList<>(); for ( final Field field : settingCollectionClass.getDeclaredFields() ) { if ( !ISetting.class.isAssignableFrom( field.getType() ) ) continue; // It's not a setting field... final ISetting< ? > setting; try { setting = (ISetting< ? >) field.get( settingCollectionClass ); } catch ( final IllegalArgumentException | IllegalAccessException e ) { // Never to happen... LEnv.LOGGER.error( "Failed to get setting field!", e ); continue; } if ( setting instanceof INodeSetting ) continue; // Page nodes are not used to store an actual value settingList.add( setting ); } return settingList; } /** Current bean version. */ public static final int BEAN_VER = 1; // Change tracking implementation /** Tells if change tracking is enabled. */ @XmlTransient private boolean trackChanges; /** Set storing the tracked setting changes. */ @XmlTransient private final Set< ISetting< ? > > changedSettingSet = new HashSet<>(); // Setting change notifier implementation /** * Map storing the setting change listeners.<br> * Key is the listened setting; the value is a {@link Set} of listeners.<br> * The value is a set-view of a {@link WeakHashMap} storing the listeners as weak-keys of the map; and it is created with * {@link Collections#newSetFromMap(Map)}. */ @XmlTransient private final Map< ISetting< ? >, Set< ISettingChangeListener > > settingChangeListenerSetMap = new HashMap<>(); // Other properties saved with the settings /** Name of module that saved the settings. */ private String savedByModuleName; /** Version of module that saved the settings. */ private VersionBean savedByModuleVersion; /** Path to save the settings to. */ @XmlTransient private Path path; /** Time when the settings were saved. */ private Date saveTime; // And finally the setting value map. Put it here last so it will be serialized last, and other properties will appear at the beginning of the output XML. // NOTE: if this map is before other properties, JAXB can't marshal it (NullPointerException is thrown...). /** * Map storing the non-default settings. Only settings diverging from the default setting values are stored here. */ @XmlJavaTypeAdapter( value = SettingsMapAdapter.class ) private final Map< ISetting< ? >, Object > settingValueMap = new HashMap<>(); /** List of valid settings that this settings bean is used for. */ @XmlTransient private List< ISetting< ? > > validSettingList; /** * Creates a new {@link SettingsBean}. */ public SettingsBean() { super( BEAN_VER ); } /** * Configures additional properties for saving. * * @param moduleName name of module that saves the settings * @param moduleVersion version of the module that saves the settings * @param path path to save the settings to */ public void configureSave( final String moduleName, final VersionBean moduleVersion, final Path path ) { this.savedByModuleName = moduleName; this.savedByModuleVersion = moduleVersion; this.path = path; } /** * Sets the list of valid settings that this settings bean is used for. * * <p> * Also purges the settings map: removes all settings that are not listed in the specified valid settings list. * </p> * * @param validSettingList the list of valid settings that this settings bean is used for to be set */ public void setValidSettingList( final List< ISetting< ? > > validSettingList ) { this.validSettingList = validSettingList; // Purge (keyset is backed by the map, removing elements from it will also remove elements from the map): settingValueMap.keySet().retainAll( validSettingList ); } @Override public List< ISetting< ? > > getValidSettingList() { return validSettingList; } @Override public String getSettingValueMapString() { return settingValueMap.toString(); } @Override public < T > T get( final ISetting< T > setting ) { // If editing setting requires registration and registration is not OK, // I do not reset the value, only return the default value. // Advantages: if registration being not OK is temporarily (e.g. reg file expired but will be dowloaded again) // custom setting values will be kept this way and restored when the new (valid) reg will is restored/redownloaded. // Returning the default value will also work when making a copy of the settings bean e.g. in the settings dialog. // Also when default value is returned, even the restore default value is disabled so the setting value will not // be changeable, so when copying back settings to the original (from the temporary edited one) and saving // will not overwrite custom value (that was set when registration was ok). if ( setting.getViewHints().isEditRequiresRegistration() && LEnv.REG_MANAGER != null && !LEnv.REG_MANAGER.isOk() ) return setting.getDefaultValue(); @SuppressWarnings( "unchecked" ) final T value = (T) settingValueMap.get( setting ); return value == null ? setting.getDefaultValue() : value; } @Override public void reset( final ISetting< ? > setting ) { if ( settingValueMap.remove( setting ) != null ) { if ( trackChanges ) changedSettingSet.add( setting ); notifyListeners( setting.selfSet() ); } } @Override public < T > void set( final ISetting< T > setting, final T value ) { // Do not store default value: if ( setting.getDefaultValue().equals( value ) ) { if ( settingValueMap.remove( setting ) != null ) { if ( trackChanges ) changedSettingSet.add( setting ); notifyListeners( setting.selfSet() ); } } else { // Previous value might have been null (default value is not stored), // so compare value to previous value (and not the other way). // If value would be default value, then we would be in the other if branch (and not on this else branch). if ( !value.equals( settingValueMap.put( setting, value ) ) ) { if ( trackChanges ) changedSettingSet.add( setting ); notifyListeners( setting.selfSet() ); } } } @Override public void addChangeListener( final ISetting< ? > setting, final ISettingChangeListener listener ) { addChangeListener( setting.selfSet(), listener ); } @Override public void addChangeListener( final Set< ? extends ISetting< ? > > settingSet, final ISettingChangeListener listener ) { for ( final ISetting< ? > setting : settingSet ) { Set< ISettingChangeListener > listenerSet = settingChangeListenerSetMap.get( setting ); if ( listenerSet == null ) settingChangeListenerSetMap.put( setting, listenerSet = Collections.newSetFromMap( new WeakHashMap< ISettingChangeListener, Boolean >() ) ); listenerSet.add( listener ); } } @Override public void addAndExecuteChangeListener( final ISetting< ? > setting, final ISettingChangeListener listener ) { addAndExecuteChangeListener( setting.selfSet(), listener ); } @Override public void addAndExecuteChangeListener( final Set< ? extends ISetting< ? > > settingSet, final ISettingChangeListener listener ) { addChangeListener( settingSet, listener ); listener.valuesChanged( new SettingChangeEvent( this, settingSet ) ); } @Override public void removeChangeListener( final ISetting< ? > setting, final ISettingChangeListener listener ) { removeChangeListener( setting.selfSet(), listener ); } @Override public void removeChangeListener( final Set< ? extends ISetting< ? > > settingSet, final ISettingChangeListener listener ) { for ( final ISetting< ? > setting : settingSet ) { final Set< ISettingChangeListener > listenerSet = settingChangeListenerSetMap.get( setting ); if ( listenerSet != null ) listenerSet.remove( listener ); } } /** * Notifies the listeners registered to the specified settings. * * @param settingSet set of settings whose values have changed */ private void notifyListeners( final Set< ? extends ISetting< ? > > settingSet ) { // Only call once each listener that is registered to any of the specified setting keys! // For this, we first collect the listeners to be called: final Set< ISettingChangeListener > listenerSet = new HashSet<>(); for ( final ISetting< ? > setting : settingSet ) { final Set< ISettingChangeListener > listenerSet2 = settingChangeListenerSetMap.get( setting ); if ( listenerSet2 != null ) listenerSet.addAll( listenerSet2 ); } if ( !listenerSet.isEmpty() ) { final SettingChangeEvent event = new SettingChangeEvent( this, settingSet ); for ( final ISettingChangeListener listener : listenerSet ) listener.valuesChanged( event ); } } /** * @see #configureSave(String, VersionBean, Path) */ @Override public void save() { saveTime = new Date(); try { JAXB.marshal( this, path.toFile() ); } catch ( final Exception e ) { e.printStackTrace(); LEnv.LOGGER.error( "Failed to save " + savedByModuleName + " settings to: " + path + "\nDo you have write permission in the folder?", e ); } } @Override public SettingsBean cloneSettings() { final SettingsBean clonedSettings; try { clonedSettings = getClass().newInstance(); } catch ( final InstantiationException | IllegalAccessException e ) { LEnv.LOGGER.error( "Failed to clone settings!", e ); return null; } clonedSettings.settingValueMap.putAll( settingValueMap ); clonedSettings.validSettingList = new ArrayList<>( validSettingList ); clonedSettings.path = path; return clonedSettings; } @Override public void copyChangedSettingsTo( final ISettingsBean targetSettings_ ) { if ( !( targetSettings_ instanceof SettingsBean ) ) throw new IllegalArgumentException( "Cannot copy settings to target: it is of differnet type!" ); final SettingsBean targetSettings = (SettingsBean) targetSettings_; if ( !path.equals( targetSettings.path ) ) throw new IllegalArgumentException( "Copying settings to a target with different save path is denied!" ); // Copy all changed settings and notify listeners in one step for optimization reasons // (listeners might perform lengthy operations when a setting changes). // Simply copying the properties is not enough, because if a setting previously was not the default value // and was changed to the default value, it is removed from the properties (and it is not copied). // Remove those by ourselves: final Map< ISetting< ? >, Object > thisSettingValueMap = this.settingValueMap; final Map< ISetting< ? >, Object > targetSettingValueMap = targetSettings.settingValueMap; for ( final ISetting< ? > setting : changedSettingSet ) { if ( thisSettingValueMap.containsKey( setting ) ) targetSettingValueMap.put( setting, thisSettingValueMap.get( setting ) ); else targetSettingValueMap.remove( setting ); } // Notify listeners using the whole changed set targetSettings.notifyListeners( changedSettingSet ); } @Override public void setTrackChanges( final boolean trackChanges ) { this.trackChanges = trackChanges; } @Override public boolean isTrackChanges() { return trackChanges; } @Override public void clearTrackedChanges() { changedSettingSet.clear(); } @Override public String getSavedByModuleName() { return savedByModuleName; } @Override public VersionBean getSavedByModuleVersion() { return savedByModuleVersion; } @Override public Date getSaveTime() { return saveTime; } }