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}