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}