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.time.format.DateTimeParseException; 031import java.util.Collections; 032import java.util.Currency; 033import java.util.Date; 034import java.util.HashSet; 035import java.util.Locale; 036import java.util.Optional; 037import java.util.Set; 038import java.util.TimeZone; 039import java.util.UUID; 040 041import static com.soklet.Utilities.trimAggressivelyToNull; 042import static java.lang.String.format; 043 044/** 045 * Non-instantiable utility class that exists to vend a set of default {@link ValueConverter} instances via {@link #defaultValueConverters()}. 046 * <p> 047 * 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>. 048 * 049 * @author <a href="https://www.revetkn.com">Mark Allen</a> 050 */ 051@ThreadSafe 052public final class ValueConverters { 053 @Nonnull 054 private static final Set<ValueConverter<?, ?>> DEFAULT_VALUE_CONVERTERS; 055 056 static { 057 DEFAULT_VALUE_CONVERTERS = Collections.unmodifiableSet(createDefaultValueConverters()); 058 } 059 060 private ValueConverters() { 061 // Cannot instantiate 062 } 063 064 /** 065 * Vends the immutable system-default set of {@link ValueConverter} instances. 066 * <p> 067 * The returned set is guaranteed to be a JVM-wide singleton. 068 * 069 * @return the default set of converters 070 */ 071 @Nonnull 072 public static Set<ValueConverter<?, ?>> defaultValueConverters() { 073 return DEFAULT_VALUE_CONVERTERS; 074 } 075 076 @Nonnull 077 private static Set<ValueConverter<?, ?>> createDefaultValueConverters() { 078 Set<ValueConverter<?, ?>> defaultValueConverters = new HashSet<>(); 079 080 // Primitives 081 defaultValueConverters.add(new StringToIntegerValueConverter()); 082 defaultValueConverters.add(new StringToLongValueConverter()); 083 defaultValueConverters.add(new StringToDoubleValueConverter()); 084 defaultValueConverters.add(new StringToFloatValueConverter()); 085 defaultValueConverters.add(new StringToByteValueConverter()); 086 defaultValueConverters.add(new StringToShortValueConverter()); 087 defaultValueConverters.add(new StringToCharacterValueConverter()); 088 defaultValueConverters.add(new StringToBooleanValueConverter()); 089 defaultValueConverters.add(new StringToBigIntegerValueConverter()); 090 defaultValueConverters.add(new StringToBigDecimalValueConverter()); 091 defaultValueConverters.add(new StringToNumberValueConverter()); 092 defaultValueConverters.add(new StringToUuidValueConverter()); 093 defaultValueConverters.add(new StringToInstantValueConverter()); 094 defaultValueConverters.add(new StringToDateValueConverter()); 095 defaultValueConverters.add(new StringToLocalDateValueConverter()); 096 defaultValueConverters.add(new StringToLocalTimeValueConverter()); 097 defaultValueConverters.add(new StringToLocalDateTimeValueConverter()); 098 defaultValueConverters.add(new StringToZoneIdValueConverter()); 099 defaultValueConverters.add(new StringToTimeZoneValueConverter()); 100 defaultValueConverters.add(new StringToLocaleValueConverter()); 101 defaultValueConverters.add(new StringToCurrencyValueConverter()); 102 103 return defaultValueConverters; 104 } 105 106 // Primitives 107 108 @ThreadSafe 109 private static final class StringToIntegerValueConverter extends FromStringValueConverter<Integer> { 110 @Override 111 @Nonnull 112 public Optional<Integer> performConversion(@Nullable String from) throws Exception { 113 if (from == null) 114 return Optional.empty(); 115 116 return Optional.of(Integer.parseInt(from)); 117 } 118 } 119 120 @ThreadSafe 121 private static final class StringToLongValueConverter extends FromStringValueConverter<Long> { 122 @Override 123 @Nonnull 124 public Optional<Long> performConversion(@Nullable String from) throws Exception { 125 if (from == null) 126 return Optional.empty(); 127 128 return Optional.of(Long.parseLong(from)); 129 } 130 } 131 132 @ThreadSafe 133 private static final class StringToDoubleValueConverter extends FromStringValueConverter<Double> { 134 @Override 135 @Nonnull 136 public Optional<Double> performConversion(@Nullable String from) throws Exception { 137 if (from == null) 138 return Optional.empty(); 139 140 return Optional.of(Double.parseDouble(from)); 141 } 142 } 143 144 @ThreadSafe 145 private static final class StringToFloatValueConverter extends FromStringValueConverter<Float> { 146 @Override 147 @Nonnull 148 public Optional<Float> performConversion(@Nullable String from) throws Exception { 149 if (from == null) 150 return Optional.empty(); 151 152 return Optional.of(Float.parseFloat(from)); 153 } 154 } 155 156 @ThreadSafe 157 private static final class StringToByteValueConverter extends FromStringValueConverter<Byte> { 158 @Override 159 @Nonnull 160 public Optional<Byte> performConversion(@Nullable String from) throws Exception { 161 if (from == null) 162 return Optional.empty(); 163 164 return Optional.of(Byte.parseByte(from)); 165 } 166 } 167 168 @ThreadSafe 169 private static final class StringToShortValueConverter extends FromStringValueConverter<Short> { 170 @Override 171 @Nonnull 172 public Optional<Short> performConversion(@Nullable String from) throws Exception { 173 if (from == null) 174 return Optional.empty(); 175 176 return Optional.of(Short.parseShort(from)); 177 } 178 } 179 180 @ThreadSafe 181 private static final class StringToCharacterValueConverter extends FromStringValueConverter<Character> { 182 @Nonnull 183 @Override 184 public Optional<Character> performConversion(@Nullable String from) throws Exception { 185 if (from == null) 186 return Optional.empty(); 187 188 String trimmedFrom = trimAggressivelyToNull(from); 189 190 // Special handling for all-whitespace. 191 // If there is at least one space, return ' ' 192 if (from.length() > 0 && trimmedFrom == null) 193 return Optional.of(' '); 194 195 if (trimmedFrom.length() != 1) 196 throw new ValueConversionException(format( 197 "Unable to convert %s value '%s' to %s. Reason: '%s' is not a single-character String.", getFromType(), trimmedFrom, 198 getToType(), from), getFromType(), from, getToType()); 199 200 return Optional.of(trimmedFrom.charAt(0)); 201 } 202 203 @Nonnull 204 @Override 205 protected Boolean shouldTrimFromValues() { 206 // Special handling: we want to handle trimming ourselves 207 return false; 208 } 209 210 @Nonnull 211 @Override 212 public Type getFromType() { 213 return String.class; 214 } 215 216 @Nonnull 217 @Override 218 public Type getToType() { 219 return Character.class; 220 } 221 } 222 223 @ThreadSafe 224 private static final class StringToBooleanValueConverter extends FromStringValueConverter<Boolean> { 225 @Override 226 @Nonnull 227 public Optional<Boolean> performConversion(@Nullable String from) throws Exception { 228 if (from == null) 229 return Optional.empty(); 230 231 boolean isTrue = "true".equalsIgnoreCase(from); 232 233 if (isTrue) 234 return Optional.of(true); 235 236 boolean isFalse = "false".equalsIgnoreCase(from); 237 238 if (isFalse) 239 return Optional.of(false); 240 241 throw new IllegalArgumentException(format("'%s' is not a valid boolean value", from)); 242 } 243 } 244 245 // Nonprimitives 246 247 @ThreadSafe 248 private static final class StringToBigIntegerValueConverter extends FromStringValueConverter<BigInteger> { 249 @Override 250 @Nonnull 251 public Optional<BigInteger> performConversion(@Nullable String from) throws Exception { 252 if (from == null) 253 return Optional.empty(); 254 255 return Optional.of(new BigInteger(from)); 256 } 257 } 258 259 @ThreadSafe 260 private static final class StringToBigDecimalValueConverter extends FromStringValueConverter<BigDecimal> { 261 @Override 262 @Nonnull 263 public Optional<BigDecimal> performConversion(@Nullable String from) throws Exception { 264 if (from == null) 265 return Optional.empty(); 266 267 return Optional.of(new BigDecimal(from)); 268 } 269 } 270 271 @ThreadSafe 272 private static final class StringToNumberValueConverter extends FromStringValueConverter<Number> { 273 @Override 274 @Nonnull 275 public Optional<Number> performConversion(@Nullable String from) throws Exception { 276 if (from == null) 277 return Optional.empty(); 278 279 return Optional.of(new BigDecimal(from)); 280 } 281 } 282 283 @ThreadSafe 284 private static final class StringToUuidValueConverter extends FromStringValueConverter<UUID> { 285 @Override 286 @Nonnull 287 public Optional<UUID> performConversion(@Nullable String from) throws Exception { 288 if (from == null) 289 return Optional.empty(); 290 291 return Optional.of(UUID.fromString(from)); 292 } 293 } 294 295 @ThreadSafe 296 private static final class StringToDateValueConverter extends FromStringValueConverter<Date> { 297 @Override 298 @Nonnull 299 public Optional<Date> performConversion(@Nullable String from) throws Exception { 300 if (from == null) 301 return Optional.empty(); 302 303 try { 304 return Optional.of(Date.from(Instant.parse(from))); 305 } catch (DateTimeParseException ignored) { 306 return Optional.of(new Date(Long.parseLong(from))); 307 } 308 } 309 } 310 311 @ThreadSafe 312 private static final class StringToInstantValueConverter extends FromStringValueConverter<Instant> { 313 @Override 314 @Nonnull 315 public Optional<Instant> performConversion(@Nullable String from) throws Exception { 316 if (from == null) 317 return Optional.empty(); 318 319 try { 320 return Optional.of(Instant.parse(from)); 321 } catch (Exception ignored) { 322 return Optional.of(Instant.ofEpochMilli(Long.parseLong(from))); 323 } 324 } 325 } 326 327 @ThreadSafe 328 private static final class StringToLocalDateValueConverter extends FromStringValueConverter<LocalDate> { 329 @Override 330 @Nonnull 331 public Optional<LocalDate> performConversion(@Nullable String from) throws Exception { 332 if (from == null) 333 return Optional.empty(); 334 335 return Optional.of(LocalDate.parse(from)); 336 } 337 } 338 339 @ThreadSafe 340 private static final class StringToLocalTimeValueConverter extends FromStringValueConverter<LocalTime> { 341 @Override 342 @Nonnull 343 public Optional<LocalTime> performConversion(@Nullable String from) throws Exception { 344 if (from == null) 345 return Optional.empty(); 346 347 return Optional.of(LocalTime.parse(from)); 348 } 349 } 350 351 @ThreadSafe 352 private static final class StringToLocalDateTimeValueConverter extends FromStringValueConverter<LocalDateTime> { 353 @Override 354 @Nonnull 355 public Optional<LocalDateTime> performConversion(@Nullable String from) throws Exception { 356 if (from == null) 357 return Optional.empty(); 358 359 return Optional.of(LocalDateTime.parse(from)); 360 } 361 } 362 363 @ThreadSafe 364 private static final class StringToZoneIdValueConverter extends FromStringValueConverter<ZoneId> { 365 @Override 366 @Nonnull 367 public Optional<ZoneId> performConversion(@Nullable String from) throws Exception { 368 if (from == null) 369 return Optional.empty(); 370 371 return Optional.of(ZoneId.of(from)); 372 } 373 } 374 375 @ThreadSafe 376 private static final class StringToTimeZoneValueConverter extends FromStringValueConverter<TimeZone> { 377 @Override 378 @Nonnull 379 public Optional<TimeZone> performConversion(@Nullable String from) throws Exception { 380 if (from == null) 381 return Optional.empty(); 382 383 // Use ZoneId.of since it will throw an exception if the format is invalid. 384 // TimeZone.getTimeZone() returns GMT for invalid formats, which is not the behavior we want 385 return Optional.of(TimeZone.getTimeZone(ZoneId.of(from))); 386 } 387 } 388 389 @ThreadSafe 390 private static final class StringToLocaleValueConverter extends FromStringValueConverter<Locale> { 391 @Override 392 @Nonnull 393 public Optional<Locale> performConversion(@Nullable String from) throws Exception { 394 if (from == null) 395 return Optional.empty(); 396 397 return Optional.of(new Locale.Builder().setLanguageTag(from).build()); 398 } 399 } 400 401 @ThreadSafe 402 private static final class StringToCurrencyValueConverter extends FromStringValueConverter<Currency> { 403 @Override 404 @Nonnull 405 public Optional<Currency> performConversion(@Nullable String from) throws Exception { 406 if (from == null) 407 return Optional.empty(); 408 409 return Optional.of(Currency.getInstance(from)); 410 } 411 } 412}