001package ezvcard.util;
002
003import java.lang.reflect.Field;
004import java.lang.reflect.Modifier;
005import java.util.ArrayList;
006import java.util.Arrays;
007import java.util.Collection;
008import java.util.Collections;
009import java.util.Objects;
010import java.util.stream.Collectors;
011
012/*
013 Copyright (c) 2012-2026, Michael Angstadt
014 All rights reserved.
015
016 Redistribution and use in source and binary forms, with or without
017 modification, are permitted provided that the following conditions are met: 
018
019 1. Redistributions of source code must retain the above copyright notice, this
020 list of conditions and the following disclaimer. 
021 2. Redistributions in binary form must reproduce the above copyright notice,
022 this list of conditions and the following disclaimer in the documentation
023 and/or other materials provided with the distribution. 
024
025 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
026 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
027 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
028 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
029 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
030 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
031 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
032 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
033 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
034 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
035 */
036
037/**
038 * <p>
039 * Manages objects that are like enums in that they are constant, but unlike
040 * enums in that new instances can be created during runtime. This class ensures
041 * that all instances of a class are unique, so they can be safely compared
042 * using "==" (provided their constructors are private).
043 * </p>
044 * <p>
045 * This class awkwardly mimics the "case class" feature in Scala.
046 * </p>
047 * <p>
048 * <b>Example:</b>
049 * </p>
050 * <pre class="brush:java">
051 * public class Color {
052 *   public static final CaseClasses&lt;Color, String&gt; VALUES = new ColorCaseClasses();
053 *   public static final Color RED = new Color("red");
054 *   public static final Color GREEN = new Color("green");
055 *   public static final Color BLUE = new Color("blue");
056 * 
057 *   private final String name;
058 * 
059 *   // Constructor should be PRIVATE in order to prevent users from
060 *   // instantiating their own objects and circumventing the CaseClasses
061 *   // object.
062 *   private Color(String name) {
063 *     this.name = name;
064 *   }
065 * 
066 *   public String getName() {
067 *     return name;
068 *   }
069 * 
070 *   // CaseClasses implementation is an inner class because the Color
071 *   // constructor is private.
072 *   private static class ColorCaseClasses extends CaseClasses&lt;Color, String&gt; {
073 *     public ColorCaseClasses() {
074 *       super(Color.class);
075 *     }
076 * 
077 *     &#64;Override
078 *     protected Color create(String value) {
079 *       return new Color(value);
080 *     }
081 * 
082 *     &#64;Override
083 *     protected boolean matches(Color object, String value) {
084 *       return object.getName().equalsIgnoreCase(value);
085 *     }
086 *   }
087 * }
088 * 
089 * public class Test {
090 *   &#64;Test
091 *   public void test() {
092 *     assertTrue(Color.RED == Color.VALUES.find("Red"));
093 *     assertTrue(Color.RED == Color.VALUES.get("Red"));
094 * 
095 *     assertNull(Color.VALUES.find("purple"));
096 *     Color purple = Color.VALUES.get("purple");
097 *     assertEquals("purple", purple.getName());
098 *     assertTrue(purple == Color.VALUES.get("Purple"));
099 *   }
100 * }
101 * </pre>
102 * @author Michael Angstadt
103 * 
104 * @param <T> the case class
105 * @param <V> the value that the class holds (e.g. String)
106 */
107public abstract class CaseClasses<T, V> {
108        protected final Class<T> clazz;
109        private volatile Collection<T> preDefined = null;
110        private Collection<T> runtimeDefined = null;
111
112        /**
113         * Creates a new case class collection.
114         * @param clazz the case class
115         */
116        protected CaseClasses(Class<T> clazz) {
117                this.clazz = clazz;
118        }
119
120        /**
121         * Creates a new instance of the case class.
122         * @param value the value to give the instance
123         * @return the new instance
124         */
125        protected abstract T create(V value);
126
127        /**
128         * Determines if a case object is "equal to" the given value.
129         * @param object the case object
130         * @param value the value
131         * @return true if it matches, false if not
132         */
133        protected abstract boolean matches(T object, V value);
134
135        /**
136         * Searches for a case object by value, only looking at the case class'
137         * static constants (does not search runtime-defined constants).
138         * @param value the value
139         * @return the object or null if one wasn't found
140         */
141        public T find(V value) {
142                checkInit();
143
144                //@formatter:off
145                return preDefined.stream()
146                        .filter(obj -> matches(obj, value))
147                .findFirst().orElse(null);
148                //@formatter:on
149        }
150
151        /**
152         * Searches for a case object by value, creating a new object if one cannot
153         * be found.
154         * @param value the value
155         * @return the object
156         */
157        public T get(V value) {
158                T found = find(value);
159                if (found != null) {
160                        return found;
161                }
162
163                synchronized (runtimeDefined) {
164                        //@formatter:off
165                        return runtimeDefined.stream()
166                                .filter(obj -> matches(obj, value))
167                        .findFirst().orElseGet(() -> {
168                                T created = create(value);
169                                runtimeDefined.add(created);
170                                return created;
171                        });
172                        //@formatter:on
173                }
174        }
175
176        /**
177         * Gets all the static constants of the case class (does not include
178         * runtime-defined constants).
179         * @return all static constants
180         */
181        public Collection<T> all() {
182                checkInit();
183                return preDefined;
184        }
185
186        /**
187         * Checks to see if this class's fields were initialized yet, and
188         * initializes them if they haven't been initialized. This method is
189         * thread-safe.
190         */
191        private void checkInit() {
192                if (preDefined == null) {
193                        synchronized (this) {
194                                //"double check idiom" (Bloch p.283)
195                                if (preDefined == null) {
196                                        init();
197                                }
198                        }
199                }
200        }
201
202        /**
203         * Initializes this class's fields.
204         */
205        private void init() {
206                //@formatter:off
207                preDefined = Collections.unmodifiableCollection(Arrays.stream(clazz.getFields())
208                        .filter(this::isPreDefinedField)
209                        .map(field -> {
210                                try {
211                                        return field.get(null);
212                                } catch (Exception e) {
213                                        //reflection error
214                                        //should never be thrown because we check for "public static" and the correct type
215                                        throw new IllegalStateException(e);
216                                }
217                        })
218                        .filter(Objects::nonNull)
219                        .map(clazz::cast)
220                .collect(Collectors.toList()));
221                //@formatter:on
222
223                runtimeDefined = new ArrayList<>(0);
224        }
225
226        /**
227         * Determines if a field should be treated as a predefined case object.
228         * @param field the field
229         * @return true if it's a predefined case object, false if not
230         */
231        private boolean isPreDefinedField(Field field) {
232                int modifiers = field.getModifiers();
233
234                //@formatter:off
235                return
236                        Modifier.isStatic(modifiers) &&
237                        Modifier.isPublic(modifiers) &&
238                        field.getDeclaringClass() == clazz &&
239                        field.getType() == clazz;
240                //@formatter:on
241        }
242}