001package squidpony; 002 003import regexodus.Matcher; 004import regexodus.Pattern; 005import squidpony.squidmath.CrossHash; 006import squidpony.squidmath.K2; 007 008import java.util.Objects; 009 010/** 011 * Used to standardize conversion for a given type, {@code T}, to and from a serialized String format. 012 * This abstract class should usually be made concrete by a single-purpose class (not the type T itself). 013 * Also includes a static registry of types as CharSequence arrays (including the classes of generic type parameters in 014 * array elements after the first) to the corresponding StringConvert objects that have been constructed for those 015 * types, although the registry must store the StringConvert objects without any further types (you can cast the 016 * StringConvert to a StringConvert with the desired generic type, or call {@link #restore(String)} on the 017 * un-parametrized type and get back an Object that can be cast to the correct type, but we aren't able to store the 018 * actual type). The static method {@link #lookup(CharSequence[])} can be used to find the StringConvert registered for 019 * a type name combination. The static method {@link #get(CharSequence)} can be used to find a StringConvert by its 020 * name. The static utility method {@link #asArray(CharSequence[])} can be used to reduce the amount of arrays produced 021 * by varargs, especially when you have a bunch of Class items and need Strings, but the array it returns must not be 022 * edited once used to construct a StringConvert. 023 */ 024public abstract class StringConvert<T> { 025 public final CharSequence name; 026 public final CharSequence[] typeNames; 027 public final String specificName; 028 public final boolean isArray; 029 private static final Matcher specificMatcher = Pattern.compile("\\p{Js}\\p{Jp}*").matcher(); 030 /** 031 * Constructs a StringConvert using a vararg or array of CharSequence objects, such as Strings, as well as a boolean 032 * flag to determine if the StringConvert works on an array instead of a normal object. If an array of types is 033 * passed, it must not be altered after usage. If no varargs are passed, if types is null, or if the first item of 034 * types is null, then this uses a special type representation where the name is "void" and typeNames has "void" as 035 * its only element. If types has length 1, then the name will be the "simple name" of the first element in types, 036 * as produced by {@link Class#getSimpleName()} (note that this produces an empty string for anonymous classes), and 037 * typeNames will again have that simple name as its only value. Otherwise, this considers items after the first to 038 * be the names of generic type arguments of the first, using normal Java syntax of {@code "Outer<A,B>"} if given 039 * the Strings for types {@code "Outer", "A", "B"}. No spaces will be present in the name, but thanks to some 040 * customization of the registry, you can give a String with spaces in it to {@link #get(CharSequence)} and still 041 * find the correct one). You can give type names with generic components as the names of generic type arguments, 042 * such as {@code new StringConvert("OrderedMap", "String", "OrderedSet<String>")} for a mapping of String keys to 043 * values that are themselves sets of Strings. After constructing a StringConvert, it is automatically registered 044 * so it can be looked up by name with {@link #get(CharSequence)} or by component generic types with 045 * {@link #lookup(CharSequence...)}; both of these will not return a StringConvert with type info for what it 046 * takes and returns beyond "Object", but the result can be cast to a StringConvert with the correct type. 047 * @param isArray true if this should convert an array type as opposed to a normal object or primitive type 048 * @param types a vararg of Class objects representing the type this can convert, including generic type parameters 049 * of the first element, if there are any, at positions after the first 050 */ 051 public StringConvert(final boolean isArray, final CharSequence... types) { 052 this.isArray = isArray; 053 if (types == null || types.length <= 0 || types[0] == null) { 054 name = "void"; 055 typeNames = new String[]{"void"}; 056 specificName = "void"; 057 } else if (types.length == 1) { 058 name = types[0]; 059 typeNames = types; 060 specificMatcher.setTarget(name); 061 if(specificMatcher.find()) 062 specificName = specificMatcher.group(); 063 else 064 specificName = "void"; 065 } else { 066 name = new StringBuilder(64); 067 ((StringBuilder) name).append(types[0]).append('<').append(types[1]); 068 for (int i = 2; i < types.length; i++) { 069 ((StringBuilder) name).append(',').append(types[i]); 070 } 071 ((StringBuilder) name).append('>'); 072 typeNames = types; 073 specificMatcher.setTarget(name); 074 if(specificMatcher.find()) 075 specificName = specificMatcher.group(); 076 else 077 specificName = "void"; 078 079 080 } 081 } 082 /** 083 * Constructs a StringConvert using a vararg or array of CharSequence objects, such as Strings. If an array is 084 * passed, it must not be altered after usage. If no arguments are passed, if types is null, or if the first item of 085 * types is null, then this uses a special type representation where the name is "void" and typeNames has "void" as 086 * its only element. If types has length 1, then the name will be the "simple name" of the first element in types, 087 * as produced by {@link Class#getSimpleName()} (note that this produces an empty string for anonymous classes), and 088 * typeNames will again have that simple name as its only value. Otherwise, this considers items after the first to 089 * be the names of generic type arguments of the first, using normal Java syntax of {@code "Outer<A,B>"} if given 090 * the Strings for types {@code "Outer", "A", "B"}. No spaces will be present in the name, but thanks to some 091 * customization of the registry, you can give a String with spaces in it to {@link #get(CharSequence)} and still 092 * find the correct one). You can give type names with generic components as the names of generic type arguments, 093 * such as {@code new StringConvert("OrderedMap", "String", "OrderedSet<String>")} for a mapping of String keys to 094 * values that are themselves sets of Strings. After constructing a StringConvert, it is automatically registered 095 * so it can be looked up by name with {@link #get(CharSequence)} or by component generic types with 096 * {@link #lookup(CharSequence...)}; both of these will not return a StringConvert with type info for what it 097 * takes and returns beyond "Object", but the result can be cast to a StringConvert with the correct type. 098 * @param types a vararg of Class objects representing the type this can convert, including generic type parameters 099 * of the first element, if there are any, at positions after the first 100 */ 101 public StringConvert(final CharSequence... types) 102 { 103 this(false, types); 104 } 105 public CharSequence getName() {return name;} 106 public abstract String stringify(T item); 107 public abstract T restore(String text); 108 109 /** 110 * Attempts to restore a specific type of value from the given text. Useful when this StringConvert does not have 111 * meaningful generic type information (e.g. {@code StringConvert<?>}), and you know the correct type externally. 112 * May throw a ClassCastException if type is not compatible with the type this deserializes to (that is, T). 113 * @param text the text to try to read as serialized data describing a T2 object 114 * @param type the Class of the data to try to produce, which should be as specific as possible 115 * @param <T2> you must be able to cast from a T (the type described by this class' {@link #specificName}) to a T2 116 * @return if this is successful, a T2 drawn from the data in text; otherwise, this may throw an exception 117 */ 118 @SuppressWarnings("unchecked") 119 public <T2> T2 restore(String text, Class<T2> type) 120 { 121 return (T2)restore(text); 122 } 123 /** 124 * Gets the registered StringConvert for the given type name, if there is one, or returns null otherwise. 125 * The name can have the normal parts of a generic type, such as "OrderedMap<String, ArrayList<String>>", 126 * as long as such a type was fully registered. For that example, you could use 127 * {@code Converters.convertOrderedMap(Converters.convertString, Converters.convertArrayList(Converters.convertString))} 128 * to produce and register a StringConvert for the aforementioned generic type. 129 * @param name the name of the type to find a registered StringConvert, such as "ArrayList<String>" or "char[]" 130 * @return the registered StringConvert, if it was found, or null if none was found 131 */ 132 public static StringConvert<?> get(final CharSequence name) 133 { 134 int i = registry.indexOfA(name); 135 if(i < 0) return null; 136 return registry.getAAt(i); 137 } 138 /** 139 * Looks up the StringConvert for a given vararg of Class instances (if an array of Classes is used other than a 140 * vararg, it must not be altered in the future, nor reused in a way that modifies its elements). Returns null if no 141 * StringConvert is found. You should usually cast the returned StringConvert, if non-null, to the specific 142 * StringConvert generic type you want. 143 * @param types the vararg of types to look up 144 * @return the StringConvert registered for the given types, or null if none has been made 145 */ 146 public static StringConvert<?> lookup(final CharSequence... types) 147 { 148 return registry.getAFromB(types); 149 } 150 151 /** 152 * Simply takes a vararg of Class and returns the simple names of the Classes as a String array. 153 * Can be handy to avoid re-creating arrays implicitly from varargs of Class items. 154 * @param types a vararg of Class 155 * @return the String simple names of types as an array 156 */ 157 public static CharSequence[] asArray(final CharSequence... types) 158 { 159 return types; 160 } 161 162 public static final CrossHash.IHasher spaceIgnoringHasher = new CrossHash.IHasher() { 163 @Override 164 public int hash(Object data) { 165 if (!(data instanceof StringConvert || data instanceof CharSequence)) 166 return 0; 167 final CharSequence s; 168 if(data instanceof StringConvert) s = ((StringConvert) data).getName(); 169 else s = (CharSequence) data; 170 171 long result = 0x1A976FDF6BF60B8EL, z = 0x60642E2A34326F15L; 172 final int len = s.length(); 173 for (int i = 0; i < len; i++) { 174 result ^= (z += (s.charAt(i) ^ 0xC6BC279692B5CC85L) * 0x6C8E9CF570932BABL); 175 result = (result << 54 | result >>> 10); 176 } 177 result += (z ^ z >>> 26) * 0x632BE59BD9B4E019L; 178 result = (result ^ result >>> 33) * 0xFF51AFD7ED558CCDL; 179 return (int) ((result ^ result >>> 33) * 0xC4CEB9FE1A85EC53L); 180 } 181 182 @Override 183 public boolean areEqual(Object left, Object right) { 184 if (left == right) return true; 185 if(!((left instanceof StringConvert || left instanceof CharSequence) 186 && (right instanceof StringConvert || right instanceof CharSequence))) 187 return false; 188 final CharSequence ls, rs; 189 if(left instanceof StringConvert) ls = ((StringConvert) left).getName(); 190 else ls = (CharSequence)left; 191 if(right instanceof StringConvert) rs = ((StringConvert) right).getName(); 192 else rs = (CharSequence)right; 193 final int llen = ls.length(), rlen = rs.length(); 194 char lc = ' ', rc = ' '; 195 for (int l = 0, r = 0; l < llen && r < rlen; l++, r++) { 196 while (l < llen && (lc = ls.charAt(l)) == ' ') ++l; 197 while (r < rlen && (rc = rs.charAt(r)) == ' ') ++r; 198 if(lc != rc) 199 return false; 200 } 201 return true; 202 } 203 }; 204 205 public static final CrossHash.IHasher spaceIgnoringArrayHasher = new CrossHash.IHasher() { 206 @Override 207 public int hash(final Object data) { 208 if (data == null) 209 return 0; 210 if(!(data instanceof CharSequence[])) 211 return data.hashCode(); 212 CharSequence[] data2 = (CharSequence[])data; 213 214 long result = 0x1A976FDF6BF60B8EL, z = 0x60642E2A34326F15L; 215 final int len = data2.length; 216 for (int i = 0; i < len; i++) { 217 result ^= (z += (spaceIgnoringHasher.hash(data2[i]) ^ 0xC6BC279692B5CC85L) * 0x6C8E9CF570932BABL); 218 result = (result << 54 | result >>> 10); 219 } 220 result += (z ^ z >>> 26) * 0x632BE59BD9B4E019L; 221 result = (result ^ result >>> 33) * 0xFF51AFD7ED558CCDL; 222 return (int) ((result ^ result >>> 33) * 0xC4CEB9FE1A85EC53L); 223 } 224 225 @Override 226 public boolean areEqual(Object left, Object right) { 227 return left == right || ((left instanceof CharSequence[] && right instanceof CharSequence[]) ? CrossHash.equalityHelper((CharSequence[]) left, (CharSequence[]) right, spaceIgnoringHasher) : Objects.equals(left, right)); 228 } 229 }; 230 231 public static final K2<StringConvert, CharSequence[]> registry = 232 new K2<>(128, 0.75f, spaceIgnoringHasher, spaceIgnoringArrayHasher); 233 234 235}