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