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<Color, String> 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<Color, String> { 073 * public ColorCaseClasses() { 074 * super(Color.class); 075 * } 076 * 077 * @Override 078 * protected Color create(String value) { 079 * return new Color(value); 080 * } 081 * 082 * @Override 083 * protected boolean matches(Color object, String value) { 084 * return object.getName().equalsIgnoreCase(value); 085 * } 086 * } 087 * } 088 * 089 * public class Test { 090 * @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}