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        // Main.pref 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 Main.pref} 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}