001/* 002 * Copyright 2022-2025 Revetware LLC. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package com.soklet.converter; 018 019import javax.annotation.Nonnull; 020import javax.annotation.Nullable; 021import javax.annotation.concurrent.ThreadSafe; 022import java.lang.reflect.Type; 023import java.math.BigDecimal; 024import java.math.BigInteger; 025import java.time.Instant; 026import java.time.LocalDate; 027import java.time.LocalDateTime; 028import java.time.LocalTime; 029import java.time.ZoneId; 030import java.util.Collections; 031import java.util.Date; 032import java.util.HashSet; 033import java.util.Locale; 034import java.util.Optional; 035import java.util.Set; 036import java.util.TimeZone; 037import java.util.UUID; 038 039import static com.soklet.core.Utilities.trimAggressivelyToNull; 040import static java.lang.String.format; 041 042/** 043 * Non-instantiable utility class that exists to vend a set of default {@link ValueConverter} instances via {@link #defaultValueConverters()}. 044 * <p> 045 * Default converters are documented at <a href="https://www.soklet.com/docs/value-conversions#default-conversions">https://www.soklet.com/docs/value-conversions#default-conversions</a>. 046 * 047 * @author <a href="https://www.revetkn.com">Mark Allen</a> 048 */ 049@ThreadSafe 050public final class ValueConverters { 051 @Nonnull 052 private static final Set<ValueConverter<?, ?>> DEFAULT_VALUE_CONVERTERS; 053 054 static { 055 DEFAULT_VALUE_CONVERTERS = Collections.unmodifiableSet(createDefaultValueConverters()); 056 } 057 058 private ValueConverters() { 059 // Cannot instantiate 060 } 061 062 /** 063 * Vends the system default set of {@link ValueConverter} instances. 064 * 065 * @return the default set of converters 066 */ 067 @Nonnull 068 public static Set<ValueConverter<?, ?>> defaultValueConverters() { 069 return DEFAULT_VALUE_CONVERTERS; 070 } 071 072 @Nonnull 073 private static Set<ValueConverter<?, ?>> createDefaultValueConverters() { 074 Set<ValueConverter<?, ?>> defaultValueConverters = new HashSet<>(); 075 076 // Primitives 077 defaultValueConverters.add(new StringToIntegerValueConverter()); 078 defaultValueConverters.add(new StringToLongValueConverter()); 079 defaultValueConverters.add(new StringToDoubleValueConverter()); 080 defaultValueConverters.add(new StringToFloatValueConverter()); 081 defaultValueConverters.add(new StringToByteValueConverter()); 082 defaultValueConverters.add(new StringToShortValueConverter()); 083 defaultValueConverters.add(new StringToCharacterValueConverter()); 084 defaultValueConverters.add(new StringToBooleanValueConverter()); 085 defaultValueConverters.add(new StringToBigIntegerValueConverter()); 086 defaultValueConverters.add(new StringToBigDecimalValueConverter()); 087 defaultValueConverters.add(new StringToNumberValueConverter()); 088 defaultValueConverters.add(new StringToUuidValueConverter()); 089 defaultValueConverters.add(new StringToInstantValueConverter()); 090 defaultValueConverters.add(new StringToDateValueConverter()); 091 defaultValueConverters.add(new StringToLocalDateValueConverter()); 092 defaultValueConverters.add(new StringToLocalTimeValueConverter()); 093 defaultValueConverters.add(new StringToLocalDateTimeValueConverter()); 094 defaultValueConverters.add(new StringToZoneIdValueConverter()); 095 defaultValueConverters.add(new StringToTimeZoneValueConverter()); 096 defaultValueConverters.add(new StringToLocaleValueConverter()); 097 098 return defaultValueConverters; 099 } 100 101 // Primitives 102 103 @ThreadSafe 104 private static final class StringToIntegerValueConverter extends FromStringValueConverter<Integer> { 105 @Override 106 @Nonnull 107 public Optional<Integer> performConversion(@Nullable String from) throws Exception { 108 if (from == null) 109 return Optional.empty(); 110 111 return Optional.of(Integer.parseInt(from)); 112 } 113 } 114 115 @ThreadSafe 116 private static final class StringToLongValueConverter extends FromStringValueConverter<Long> { 117 @Override 118 @Nonnull 119 public Optional<Long> performConversion(@Nullable String from) throws Exception { 120 if (from == null) 121 return Optional.empty(); 122 123 return Optional.of(Long.parseLong(from)); 124 } 125 } 126 127 @ThreadSafe 128 private static final class StringToDoubleValueConverter extends FromStringValueConverter<Double> { 129 @Override 130 @Nonnull 131 public Optional<Double> performConversion(@Nullable String from) throws Exception { 132 if (from == null) 133 return Optional.empty(); 134 135 return Optional.of(Double.parseDouble(from)); 136 } 137 } 138 139 @ThreadSafe 140 private static final class StringToFloatValueConverter extends FromStringValueConverter<Float> { 141 @Override 142 @Nonnull 143 public Optional<Float> performConversion(@Nullable String from) throws Exception { 144 if (from == null) 145 return Optional.empty(); 146 147 return Optional.of(Float.parseFloat(from)); 148 } 149 } 150 151 @ThreadSafe 152 private static final class StringToByteValueConverter extends FromStringValueConverter<Byte> { 153 @Override 154 @Nonnull 155 public Optional<Byte> performConversion(@Nullable String from) throws Exception { 156 if (from == null) 157 return Optional.empty(); 158 159 return Optional.of(Byte.parseByte(from)); 160 } 161 } 162 163 @ThreadSafe 164 private static final class StringToShortValueConverter extends FromStringValueConverter<Short> { 165 @Override 166 @Nonnull 167 public Optional<Short> performConversion(@Nullable String from) throws Exception { 168 if (from == null) 169 return Optional.empty(); 170 171 return Optional.of(Short.parseShort(from)); 172 } 173 } 174 175 @ThreadSafe 176 private static final class StringToCharacterValueConverter extends FromStringValueConverter<Character> { 177 @Nonnull 178 @Override 179 public Optional<Character> performConversion(@Nullable String from) throws Exception { 180 if (from == null) 181 return Optional.empty(); 182 183 String trimmedFrom = trimAggressivelyToNull(from); 184 185 // Special handling for all-whitespace. 186 // If there is at least one space, return ' ' 187 if (from.length() > 0 && trimmedFrom == null) 188 return Optional.of(' '); 189 190 if (trimmedFrom.length() != 1) 191 throw new ValueConversionException(format( 192 "Unable to convert %s value '%s' to %s. Reason: '%s' is not a single-character String.", getFromType(), trimmedFrom, 193 getToType(), from), getFromType(), from, getToType()); 194 195 return Optional.of(trimmedFrom.charAt(0)); 196 } 197 198 @Nonnull 199 @Override 200 protected Boolean shouldTrimFromValues() { 201 // Special handling: we want to handle trimming ourselves 202 return false; 203 } 204 205 @Nonnull 206 @Override 207 public Type getFromType() { 208 return String.class; 209 } 210 211 @Nonnull 212 @Override 213 public Type getToType() { 214 return Character.class; 215 } 216 } 217 218 @ThreadSafe 219 private static final class StringToBooleanValueConverter extends FromStringValueConverter<Boolean> { 220 @Override 221 @Nonnull 222 public Optional<Boolean> performConversion(@Nullable String from) throws Exception { 223 if (from == null) 224 return Optional.empty(); 225 226 return Optional.of(Boolean.parseBoolean(from)); 227 } 228 } 229 230 // Nonprimitives 231 232 @ThreadSafe 233 private static final class StringToBigIntegerValueConverter extends FromStringValueConverter<BigInteger> { 234 @Override 235 @Nonnull 236 public Optional<BigInteger> performConversion(@Nullable String from) throws Exception { 237 if (from == null) 238 return Optional.empty(); 239 240 return Optional.of(new BigInteger(from)); 241 } 242 } 243 244 @ThreadSafe 245 private static final class StringToBigDecimalValueConverter extends FromStringValueConverter<BigDecimal> { 246 @Override 247 @Nonnull 248 public Optional<BigDecimal> performConversion(@Nullable String from) throws Exception { 249 if (from == null) 250 return Optional.empty(); 251 252 return Optional.of(new BigDecimal(from)); 253 } 254 } 255 256 @ThreadSafe 257 private static final class StringToNumberValueConverter extends FromStringValueConverter<Number> { 258 @Override 259 @Nonnull 260 public Optional<Number> performConversion(@Nullable String from) throws Exception { 261 if (from == null) 262 return Optional.empty(); 263 264 return Optional.of(new BigDecimal(from)); 265 } 266 } 267 268 @ThreadSafe 269 private static final class StringToUuidValueConverter extends FromStringValueConverter<UUID> { 270 @Override 271 @Nonnull 272 public Optional<UUID> performConversion(@Nullable String from) throws Exception { 273 if (from == null) 274 return Optional.empty(); 275 276 return Optional.of(UUID.fromString(from)); 277 } 278 } 279 280 @ThreadSafe 281 private static final class StringToDateValueConverter extends FromStringValueConverter<Date> { 282 @Override 283 @Nonnull 284 public Optional<Date> performConversion(@Nullable String from) throws Exception { 285 if (from == null) 286 return Optional.empty(); 287 288 return Optional.of(new Date(Long.parseLong(from))); 289 } 290 } 291 292 @ThreadSafe 293 private static final class StringToInstantValueConverter extends FromStringValueConverter<Instant> { 294 @Override 295 @Nonnull 296 public Optional<Instant> performConversion(@Nullable String from) throws Exception { 297 if (from == null) 298 return Optional.empty(); 299 300 return Optional.of(Instant.ofEpochMilli(Long.parseLong(from))); 301 } 302 } 303 304 @ThreadSafe 305 private static final class StringToLocalDateValueConverter extends FromStringValueConverter<LocalDate> { 306 @Override 307 @Nonnull 308 public Optional<LocalDate> performConversion(@Nullable String from) throws Exception { 309 if (from == null) 310 return Optional.empty(); 311 312 return Optional.of(LocalDate.parse(from)); 313 } 314 } 315 316 @ThreadSafe 317 private static final class StringToLocalTimeValueConverter extends FromStringValueConverter<LocalTime> { 318 @Override 319 @Nonnull 320 public Optional<LocalTime> performConversion(@Nullable String from) throws Exception { 321 if (from == null) 322 return Optional.empty(); 323 324 return Optional.of(LocalTime.parse(from)); 325 } 326 } 327 328 @ThreadSafe 329 private static final class StringToLocalDateTimeValueConverter extends FromStringValueConverter<LocalDateTime> { 330 @Override 331 @Nonnull 332 public Optional<LocalDateTime> performConversion(@Nullable String from) throws Exception { 333 if (from == null) 334 return Optional.empty(); 335 336 return Optional.of(LocalDateTime.parse(from)); 337 } 338 } 339 340 @ThreadSafe 341 private static final class StringToZoneIdValueConverter extends FromStringValueConverter<ZoneId> { 342 @Override 343 @Nonnull 344 public Optional<ZoneId> performConversion(@Nullable String from) throws Exception { 345 if (from == null) 346 return Optional.empty(); 347 348 return Optional.of(ZoneId.of(from)); 349 } 350 } 351 352 @ThreadSafe 353 private static final class StringToTimeZoneValueConverter extends FromStringValueConverter<TimeZone> { 354 @Override 355 @Nonnull 356 public Optional<TimeZone> performConversion(@Nullable String from) throws Exception { 357 if (from == null) 358 return Optional.empty(); 359 360 // Use ZoneId.of since it will throw an exception if the format is invalid. 361 // TimeZone.getTimeZone() returns GMT for invalid formats, which is not the behavior we want 362 return Optional.of(TimeZone.getTimeZone(ZoneId.of(from))); 363 } 364 } 365 366 @ThreadSafe 367 private static final class StringToLocaleValueConverter extends FromStringValueConverter<Locale> { 368 @Override 369 @Nonnull 370 public Optional<Locale> performConversion(@Nullable String from) throws Exception { 371 if (from == null) 372 return Optional.empty(); 373 374 return Optional.of(new Locale.Builder().setLanguageTag(from).build()); 375 } 376 } 377}