001/*******************************************************************************
002 * This software is provided as a supplement to the authors' textbooks on digital
003 * image processing published by Springer-Verlag in various languages and editions.
004 * Permission to use and distribute this software is granted under the BSD 2-Clause
005 * "Simplified" License (see http://opensource.org/licenses/BSD-2-Clause).
006 * Copyright (c) 2006-2023 Wilhelm Burger, Mark J. Burge. All rights reserved.
007 * Visit https://imagingbook.com for additional details.
008 ******************************************************************************/
009package imagingbook.common.util;
010
011import java.util.HashMap;
012import java.util.Objects;
013
014
015/**
016 * Defines a primitive mechanism for attaching arbitrary properties to an object dynamically using generic types to
017 * eliminate type casts. Objects of any type may be attached and some type checking is done at compile time. The keys to
018 * be used for inserting and retrieving values are identified by name (a {@link String}) and associated with a specific
019 * value type. An implementing class only needs to define method {@link #getPropertyMap()}. In principle, the
020 * functionality is the same as {@link HashMap} but definition as an interface avoids having to subclass
021 * {@link HashMap}. Typical usage example:
022 * <pre>
023 *     public class Foo implements GenericProperties {
024 *          private final PropertyMap properties = new PropertyMap();
025 *          &#64;Override
026 *          public PropertyMap getPropertyMap() {
027 *              return this.properties;
028 *          }
029 *          ...
030 *     }
031 *     Foo f = new Foo();
032 *     PropertyKey&lt;double[]&gt; key = new PropertyKey&lt;&gt;("UniqueName");
033 *     double[] x = {1, 2, 3, 4};
034 *     f.setProperty(key, x);
035 *     ...
036 *     double[] y = f.getProperty(key);
037 * </pre>
038 * Note that only values matching the key's type can be passed to {@link #setProperty(PropertyKey, Object)} and no type
039 * casts are required when using {@link #getProperty(PropertyKey)}. However, creating duplicate keys with the same name
040 * but different type is an error and must be avoided (unfortunately this cannot be checked at compile time and
041 * {@link PropertyKey} has no information about the associated value class at runtime).
042 *
043 * @author WB
044 * @version 2023/01/03
045 */
046public interface GenericProperties {
047
048    /**
049     * Defines a generic map key to be used with {@link GenericProperties}.
050     * @param <T> the generic key type
051     */
052    public final class PropertyKey<T> {
053        private final String name;
054
055        public PropertyKey(String name) {
056            this.name = name;
057        }
058    }
059
060    /**
061     * The underlying hash map class, to be instantiated by implementing classes.
062     */
063    public final class PropertyMap extends HashMap<String, Object> {
064        public PropertyMap() {
065            super(4);   // start with up to 4 properties
066        }
067    }
068
069    /**
070     * Returns an instance of {@link PropertyMap} with keys of type {@link String} and values of type {@link Object}.
071     * Implementing classes must define this method, which will typically return a reference to a local map instance.
072     * The returned object must not be {@code null}.
073     *
074     * @return the {@link PropertyMap} associated with the implementing instance
075     */
076    public PropertyMap getPropertyMap();
077
078    /**
079     * Associates the specified value with the specified key in this property map. The supplied value must match the
080     * generic type of key. If the property map previously contained an entry for that key, the old value is replaced by
081     * the specified value if it is of the same type as the new value. Otherwise, if the type of the existing entry is
082     * different to the type of the new value, an exception is thrown. This happens when two {@link PropertyKey} with the
083     * same name but of different value type are used (which is an error).
084     *
085     * @param key the key of the property (may not be {@code null})
086     * @param value the value associated with this property (may not be {@code null})
087     * @param <T> the generic key and value type
088     * @throws IllegalArgumentException if the supplied key or value is {@code null} or if the property map already
089     * contains an entry with a different type
090     */
091    public default <T> void setProperty(PropertyKey<T> key, T value) {
092        if (Objects.isNull(key)) {
093            throw new IllegalArgumentException("property key must not be null");
094        }
095        if (Objects.isNull(value)) {
096            throw new IllegalArgumentException("property value must not be null");
097        }
098        PropertyMap map = getPropertyMap();
099        Object oldval = map.get(key.name);
100        if (oldval != null && !oldval.getClass().equals(value.getClass())) {
101            throw new IllegalArgumentException("duplicate map key " + key.name + " with value of type " +
102                            oldval.getClass().getSimpleName());
103        }
104        map.put(key.name, value);
105    }
106
107    /**
108     * Returns the value associated with the specified {@link PropertyKey}, or {@code null} if this map contains no
109     * mapping for the key.
110     *
111     * @param key the name of the property (may not be {@code null})
112     * @param <T> the generic key and value type
113     * @return the value (of type T) to which the specified key is mapped, or {@code null} if this map contains no
114     * mapping for the key
115     * @throws IllegalArgumentException if the supplied key is {@code null}
116     */
117    public default <T> T getProperty(PropertyKey<T> key) {
118        if (key == null) {
119            throw new IllegalArgumentException("property key must not be null");
120        }
121        return (T) getPropertyMap().get(key.name);
122    }
123
124    /**
125     * Removes the property associated with the specified key if defined, otherwise does nothing.
126     * @param key the name of the property
127     * @param <T> the generic key type
128     */
129    public default <T> void removeProperty(PropertyKey<T> key) {
130        getPropertyMap().remove(key.name);
131    }
132
133    /**
134     * Removes all properties.
135     */
136    public default void clearAllProperties() {
137        getPropertyMap().clear();
138    }
139
140}