001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data.preferences; 003 004import java.util.Objects; 005 006import org.openstreetmap.josm.spi.preferences.Config; 007import org.openstreetmap.josm.spi.preferences.IPreferences; 008import org.openstreetmap.josm.spi.preferences.PreferenceChangeEvent; 009import org.openstreetmap.josm.spi.preferences.PreferenceChangedListener; 010import org.openstreetmap.josm.tools.ListenableWeakReference; 011import org.openstreetmap.josm.tools.bugreport.BugReport; 012 013/** 014 * Captures the common functionality of preference properties 015 * @param <T> The type of object accessed by this property 016 */ 017public abstract class AbstractProperty<T> { 018 019 private final class PreferenceChangedListenerAdapter implements PreferenceChangedListener { 020 private final ValueChangeListener<? super T> listener; 021 022 PreferenceChangedListenerAdapter(ValueChangeListener<? super T> listener) { 023 this.listener = listener; 024 } 025 026 @Override 027 public void preferenceChanged(PreferenceChangeEvent e) { 028 listener.valueChanged(new ValueChangeEvent<>(e, AbstractProperty.this)); 029 } 030 031 @Override 032 public int hashCode() { 033 return Objects.hash(getOuterType(), listener); 034 } 035 036 @Override 037 public boolean equals(Object obj) { 038 if (this == obj) 039 return true; 040 if (obj == null || getClass() != obj.getClass()) 041 return false; 042 @SuppressWarnings("unchecked") 043 PreferenceChangedListenerAdapter other = (PreferenceChangedListenerAdapter) obj; 044 if (!getOuterType().equals(other.getOuterType())) 045 return false; 046 if (listener == null) { 047 if (other.listener != null) 048 return false; 049 } else if (!listener.equals(other.listener)) 050 return false; 051 return true; 052 } 053 054 private AbstractProperty<T> getOuterType() { 055 return AbstractProperty.this; 056 } 057 058 @Override 059 public String toString() { 060 return "PreferenceChangedListenerAdapter [listener=" + listener + ']'; 061 } 062 } 063 064 /** 065 * A listener that listens to changes in the properties value. 066 * @author michael 067 * @param <T> property type 068 * @since 10824 069 */ 070 @FunctionalInterface 071 public interface ValueChangeListener<T> { 072 /** 073 * Method called when a property value has changed. 074 * @param e property change event 075 */ 076 void valueChanged(ValueChangeEvent<? extends T> e); 077 } 078 079 /** 080 * An event that is triggered if the value of a property changes. 081 * @author Michael Zangl 082 * @param <T> property type 083 * @since 10824 084 */ 085 public static class ValueChangeEvent<T> { 086 private final PreferenceChangeEvent base; 087 private final AbstractProperty<T> source; 088 089 ValueChangeEvent(PreferenceChangeEvent base, AbstractProperty<T> source) { 090 this.base = base; 091 this.source = source; 092 } 093 094 /** 095 * Get the base event. 096 * @return the base event 097 * @since 11496 098 */ 099 public final PreferenceChangeEvent getBaseEvent() { 100 return base; 101 } 102 103 /** 104 * Get the property that was changed 105 * @return The property. 106 */ 107 public AbstractProperty<T> getProperty() { 108 return source; 109 } 110 } 111 112 /** 113 * An exception that is thrown if a preference value is invalid. 114 * @author Michael Zangl 115 * @since 10824 116 */ 117 public static class InvalidPreferenceValueException extends RuntimeException { 118 119 /** 120 * Constructs a new {@code InvalidPreferenceValueException} with the specified detail message and cause. 121 * @param message the detail message (which is saved for later retrieval by the {@link #getMessage()} method). 122 * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). 123 */ 124 public InvalidPreferenceValueException(String message, Throwable cause) { 125 super(message, cause); 126 } 127 128 /** 129 * Constructs a new {@code InvalidPreferenceValueException} with the specified detail message. 130 * The cause is not initialized, and may subsequently be initialized by a call to {@link #initCause}. 131 * 132 * @param message the detail message. The detail message is saved for later retrieval by the {@link #getMessage()} method. 133 */ 134 public InvalidPreferenceValueException(String message) { 135 super(message); 136 } 137 138 /** 139 * Constructs a new {@code InvalidPreferenceValueException} with the specified cause and a detail message of 140 * <code>(cause==null ? null : cause.toString())</code> (which typically contains the class and detail message of <code>cause</code>). 141 * 142 * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). 143 */ 144 public InvalidPreferenceValueException(Throwable cause) { 145 super(cause); 146 } 147 } 148 149 /** 150 * The preferences object this property is for. 151 */ 152 protected final IPreferences preferences; 153 protected final String key; 154 protected final T defaultValue; 155 156 /** 157 * Constructs a new {@code AbstractProperty}. 158 * @param key The property key 159 * @param defaultValue The default value 160 * @since 5464 161 */ 162 protected AbstractProperty(String key, T defaultValue) { 163 // Config.getPref() should not change in production but may change during tests. 164 preferences = Config.getPref(); 165 this.key = key; 166 this.defaultValue = defaultValue; 167 } 168 169 /** 170 * Store the default value to the preferences. 171 */ 172 protected void storeDefaultValue() { 173 if (getPreferences() != null) { 174 get(); 175 } 176 } 177 178 /** 179 * Replies the property key. 180 * @return The property key 181 */ 182 public String getKey() { 183 return key; 184 } 185 186 /** 187 * Determines if this property is currently set in JOSM preferences. 188 * @return true if {@code getPreferences()} contains this property. 189 */ 190 public boolean isSet() { 191 return getPreferences().getKeySet().contains(key); 192 } 193 194 /** 195 * Replies the default value of this property. 196 * @return The default value of this property 197 */ 198 public T getDefaultValue() { 199 return defaultValue; 200 } 201 202 /** 203 * Removes this property from JOSM preferences (i.e replace it by its default value). 204 */ 205 public void remove() { 206 getPreferences().put(key, null); 207 } 208 209 /** 210 * Replies the value of this property. 211 * @return the value of this property 212 * @since 5464 213 */ 214 public abstract T get(); 215 216 /** 217 * Sets this property to the specified value. 218 * @param value The new value of this property 219 * @return true if something has changed (i.e. value is different than before) 220 * @since 5464 221 */ 222 public abstract boolean put(T value); 223 224 /** 225 * Gets the preferences used for this property. 226 * @return The preferences for this property. 227 * @since 12999 228 */ 229 protected IPreferences getPreferences() { 230 return preferences; 231 } 232 233 /** 234 * Creates a new {@link CachingProperty} instance for this property. 235 * @return The new caching property instance. 236 * @since 12983 237 */ 238 public CachingProperty<T> cached() { 239 return new CachingProperty<>(this); 240 } 241 242 /** 243 * Adds a listener that listens only for changes to this preference key. 244 * @param listener The listener to add. 245 * @since 10824 246 */ 247 public void addListener(ValueChangeListener<? super T> listener) { 248 try { 249 addListenerImpl(new PreferenceChangedListenerAdapter(listener)); 250 } catch (RuntimeException e) { 251 throw BugReport.intercept(e).put("listener", listener).put("preference", key); 252 } 253 } 254 255 protected void addListenerImpl(PreferenceChangedListener adapter) { 256 getPreferences().addKeyPreferenceChangeListener(getKey(), adapter); 257 } 258 259 /** 260 * Adds a weak listener that listens only for changes to this preference key. 261 * @param listener The listener to add. 262 * @since 10824 263 */ 264 public void addWeakListener(ValueChangeListener<? super T> listener) { 265 try { 266 ValueChangeListener<T> weakListener = new WeakPreferenceAdapter(listener); 267 PreferenceChangedListenerAdapter adapter = new PreferenceChangedListenerAdapter(weakListener); 268 addListenerImpl(adapter); 269 } catch (RuntimeException e) { 270 throw BugReport.intercept(e).put("listener", listener).put("preference", key); 271 } 272 } 273 274 /** 275 * This class wraps the ValueChangeListener in a ListenableWeakReference that automatically removes itself 276 * if the listener is garbage collected. 277 * @author Michael Zangl 278 */ 279 private class WeakPreferenceAdapter extends ListenableWeakReference<ValueChangeListener<? super T>> 280 implements ValueChangeListener<T> { 281 WeakPreferenceAdapter(ValueChangeListener<? super T> referent) { 282 super(referent); 283 } 284 285 @Override 286 public void valueChanged(ValueChangeEvent<? extends T> e) { 287 ValueChangeListener<? super T> r = super.get(); 288 if (r != null) { 289 r.valueChanged(e); 290 } 291 } 292 293 @Override 294 protected void onDereference() { 295 removeListenerImpl(new PreferenceChangedListenerAdapter(this)); 296 } 297 } 298 299 /** 300 * Removes a listener that listens only for changes to this preference key. 301 * @param listener The listener to add. 302 * @since 10824 303 */ 304 public void removeListener(ValueChangeListener<? super T> listener) { 305 try { 306 removeListenerImpl(new PreferenceChangedListenerAdapter(listener)); 307 } catch (RuntimeException e) { 308 throw BugReport.intercept(e).put("listener", listener).put("preference", key); 309 } 310 } 311 312 protected void removeListenerImpl(PreferenceChangedListener adapter) { 313 getPreferences().removeKeyPreferenceChangeListener(getKey(), adapter); 314 } 315 316 @Override 317 public int hashCode() { 318 return Objects.hash(key, preferences); 319 } 320 321 @Override 322 public boolean equals(Object obj) { 323 if (this == obj) 324 return true; 325 if (obj == null || getClass() != obj.getClass()) 326 return false; 327 AbstractProperty<?> other = (AbstractProperty<?>) obj; 328 if (key == null) { 329 if (other.key != null) 330 return false; 331 } else if (!key.equals(other.key)) 332 return false; 333 if (preferences == null) { 334 if (other.preferences != null) 335 return false; 336 } else if (!preferences.equals(other.preferences)) 337 return false; 338 return true; 339 } 340}