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}