001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.preferences;
003
004import org.openstreetmap.josm.Main;
005import org.openstreetmap.josm.data.Preferences;
006import org.openstreetmap.josm.data.Preferences.PreferenceChangeEvent;
007import org.openstreetmap.josm.data.Preferences.PreferenceChangedListener;
008
009/**
010 * Captures the common functionality of preference properties
011 * @param <T> The type of object accessed by this property
012 */
013public abstract class AbstractProperty<T> {
014
015    private final class PreferenceChangedListenerAdapter implements PreferenceChangedListener {
016        private final ValueChangeListener<? super T> listener;
017
018        PreferenceChangedListenerAdapter(ValueChangeListener<? super T> listener) {
019            this.listener = listener;
020        }
021
022        @Override
023        public void preferenceChanged(PreferenceChangeEvent e) {
024            listener.valueChanged(new ValueChangeEvent<>(e, AbstractProperty.this));
025        }
026
027        @Override
028        public int hashCode() {
029            final int prime = 31;
030            int result = 1;
031            result = prime * result + getOuterType().hashCode();
032            result = prime * result + ((listener == null) ? 0 : listener.hashCode());
033            return result;
034        }
035
036        @Override
037        public boolean equals(Object obj) {
038            if (this == obj)
039                return true;
040            if (obj == null)
041                return false;
042            if (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
090        private final AbstractProperty<T> source;
091
092        ValueChangeEvent(PreferenceChangeEvent base, AbstractProperty<T> source) {
093            this.base = base;
094            this.source = source;
095        }
096
097        /**
098         * Get the property that was changed
099         * @return The property.
100         */
101        public AbstractProperty<T> getProperty() {
102            return source;
103        }
104    }
105
106    /**
107     * An exception that is thrown if a preference value is invalid.
108     * @author Michael Zangl
109     * @since 10824
110     */
111    public static class InvalidPreferenceValueException extends RuntimeException {
112
113        /**
114         * Constructs a new {@code InvalidPreferenceValueException} with the specified detail message and cause.
115         * @param message the detail message (which is saved for later retrieval by the {@link #getMessage()} method).
116         * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method).
117         */
118        public InvalidPreferenceValueException(String message, Throwable cause) {
119            super(message, cause);
120        }
121
122        /**
123         * Constructs a new {@code InvalidPreferenceValueException} with the specified detail message.
124         * The cause is not initialized, and may subsequently be initialized by a call to {@link #initCause}.
125         *
126         * @param message the detail message. The detail message is saved for later retrieval by the {@link #getMessage()} method.
127         */
128        public InvalidPreferenceValueException(String message) {
129            super(message);
130        }
131
132        /**
133         * Constructs a new {@code InvalidPreferenceValueException} with the specified cause and a detail message of
134         * <tt>(cause==null ? null : cause.toString())</tt> (which typically contains the class and detail message of <tt>cause</tt>).
135         *
136         * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method).
137         */
138        public InvalidPreferenceValueException(Throwable cause) {
139            super(cause);
140        }
141    }
142
143    /**
144     * The preferences object this property is for.
145     */
146    protected final Preferences preferences;
147    protected final String key;
148    protected final T defaultValue;
149
150    /**
151     * Constructs a new {@code AbstractProperty}.
152     * @param key The property key
153     * @param defaultValue The default value
154     * @since 5464
155     */
156    public AbstractProperty(String key, T defaultValue) {
157        // Main.pref should not change in production but may change during tests.
158        preferences = Main.pref;
159        this.key = key;
160        this.defaultValue = defaultValue;
161    }
162
163    /**
164     * Store the default value to {@link Preferences}.
165     */
166    protected void storeDefaultValue() {
167        if (getPreferences() != null) {
168            get();
169        }
170    }
171
172    /**
173     * Replies the property key.
174     * @return The property key
175     */
176    public String getKey() {
177        return key;
178    }
179
180    /**
181     * Determines if this property is currently set in JOSM preferences.
182     * @return true if {@code Main.pref} contains this property.
183     */
184    public boolean isSet() {
185        return !getPreferences().get(key).isEmpty();
186    }
187
188    /**
189     * Replies the default value of this property.
190     * @return The default value of this property
191     */
192    public T getDefaultValue() {
193        return defaultValue;
194    }
195
196    /**
197     * Removes this property from JOSM preferences (i.e replace it by its default value).
198     */
199    public void remove() {
200        put(getDefaultValue());
201    }
202
203    /**
204     * Replies the value of this property.
205     * @return the value of this property
206     * @since 5464
207     */
208    public abstract T get();
209
210    /**
211     * Sets this property to the specified value.
212     * @param value The new value of this property
213     * @return true if something has changed (i.e. value is different than before)
214     * @since 5464
215     */
216    public abstract boolean put(T value);
217
218    /**
219     * Gets the preferences used for this property.
220     * @return The preferences for this property.
221     * @since 10824
222     */
223    protected Preferences getPreferences() {
224        return preferences;
225    }
226
227    /**
228     * Adds a listener that listens only for changes to this preference key.
229     * @param listener The listener to add.
230     * @since 10824
231     */
232    public void addListener(ValueChangeListener<? super T> listener) {
233        addListenerImpl(new PreferenceChangedListenerAdapter(listener));
234    }
235
236    protected void addListenerImpl(PreferenceChangedListener adapter) {
237        getPreferences().addKeyPreferenceChangeListener(getKey(), adapter);
238    }
239
240    /**
241     * Adds a weak listener that listens only for changes to this preference key.
242     * @param listener The listener to add.
243     * @since 10824
244     */
245    public void addWeakListener(ValueChangeListener<? super T> listener) {
246        addWeakListenerImpl(new PreferenceChangedListenerAdapter(listener));
247    }
248
249    protected void addWeakListenerImpl(PreferenceChangedListener adapter) {
250        getPreferences().addWeakKeyPreferenceChangeListener(getKey(), adapter);
251    }
252
253    /**
254     * Removes a listener that listens only for changes to this preference key.
255     * @param listener The listener to add.
256     * @since 10824
257     */
258    public void removeListener(ValueChangeListener<? super T> listener) {
259        removeListenerImpl(new PreferenceChangedListenerAdapter(listener));
260    }
261
262    protected void removeListenerImpl(PreferenceChangedListener adapter) {
263        getPreferences().removeKeyPreferenceChangeListener(getKey(), adapter);
264    }
265
266    @Override
267    public int hashCode() {
268        final int prime = 31;
269        int result = 1;
270        result = prime * result + ((key == null) ? 0 : key.hashCode());
271        result = prime * result + ((preferences == null) ? 0 : preferences.hashCode());
272        return result;
273    }
274
275    @Override
276    public boolean equals(Object obj) {
277        if (this == obj)
278            return true;
279        if (obj == null)
280            return false;
281        if (getClass() != obj.getClass())
282            return false;
283        AbstractProperty<?> other = (AbstractProperty<?>) obj;
284        if (key == null) {
285            if (other.key != null)
286                return false;
287        } else if (!key.equals(other.key))
288            return false;
289        if (preferences == null) {
290            if (other.preferences != null)
291                return false;
292        } else if (!preferences.equals(other.preferences))
293            return false;
294        return true;
295    }
296}