001/**
002 * Copyright (C) 2006-2025 Talend Inc. - www.talend.com
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 */
016package org.talend.sdk.component.runtime.record;
017
018import java.math.BigDecimal;
019import java.sql.Timestamp;
020import java.time.Instant;
021import java.time.ZoneId;
022import java.time.ZonedDateTime;
023import java.util.AbstractMap;
024import java.util.Base64;
025import java.util.Date;
026import java.util.Map;
027import java.util.stream.Collectors;
028import java.util.stream.Stream;
029
030import lombok.extern.slf4j.Slf4j;
031
032@Slf4j
033public class MappingUtils {
034
035    private static final ZoneId UTC = ZoneId.of("UTC");
036
037    private static final Map<Class<?>, Class<?>> PRIMITIVE_WRAPPER_MAP = Stream
038            .of(new AbstractMap.SimpleImmutableEntry<>(boolean.class, Boolean.class),
039                    new AbstractMap.SimpleImmutableEntry<>(byte.class, Byte.class),
040                    new AbstractMap.SimpleImmutableEntry<>(char.class, Character.class),
041                    new AbstractMap.SimpleImmutableEntry<>(double.class, Double.class),
042                    new AbstractMap.SimpleImmutableEntry<>(float.class, Float.class),
043                    new AbstractMap.SimpleImmutableEntry<>(int.class, Integer.class),
044                    new AbstractMap.SimpleImmutableEntry<>(long.class, Long.class),
045                    new AbstractMap.SimpleImmutableEntry<>(short.class, Short.class))
046            .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
047
048    public static <T> Object coerce(final Class<T> expectedType, final Object value, final String name) {
049        log.trace("[coerce] expectedType={}, value={}, name={}.", expectedType, value, name);
050        // null is null, la la la la la... guess which song is it ;-)
051        if (value == null) {
052            return null;
053        }
054        // datetime cases from Long
055        if (Long.class.isInstance(value) && expectedType != Long.class) {
056            if (ZonedDateTime.class == expectedType) {
057                final long epochMilli = Number.class.cast(value).longValue();
058                return ZonedDateTime.ofInstant(Instant.ofEpochMilli(epochMilli), UTC);
059            }
060            if (Date.class == expectedType) {
061                return new Date(Number.class.cast(value).longValue());
062            }
063            if (Instant.class == expectedType) {
064                return Instant.ofEpochMilli(Number.class.cast(value).longValue());
065            }
066        }
067
068        // non-matching types
069        if (!expectedType.isInstance(value)) {
070            // number classes mapping
071            if (Number.class.isInstance(value)
072                    && Number.class.isAssignableFrom(PRIMITIVE_WRAPPER_MAP.getOrDefault(expectedType, expectedType))) {
073                return mapNumber(expectedType, Number.class.cast(value));
074            }
075            // mapping primitive <-> Class
076            if (isAssignableTo(value.getClass(), expectedType)) {
077                return mapPrimitiveWrapper(expectedType, value);
078            }
079            if (String.class == expectedType) {
080                return String.valueOf(value);
081            }
082            // TCOMP-2293 support Instant
083            if (Instant.class.isInstance(value)) {
084                if (ZonedDateTime.class == expectedType) {
085                    return ZonedDateTime.ofInstant((Instant) value, UTC);
086                } else if (java.util.Date.class == expectedType) {
087                    return java.sql.Timestamp.from((Instant) value);
088                } else if (Long.class == expectedType) {
089                    return ((Instant) value).toEpochMilli();
090                }
091            }
092            if (Timestamp.class.isInstance(value)
093                    && (java.util.Date.class == expectedType || Instant.class == expectedType)) {
094                return value;
095            }
096            if (value instanceof long[]) {
097                final Instant instant = Instant.ofEpochSecond(((long[]) value)[0], ((long[]) value)[1]);
098                if (ZonedDateTime.class == expectedType) {
099                    return ZonedDateTime.ofInstant(instant, UTC);
100                }
101                if (Instant.class == expectedType) {
102                    return instant;
103                }
104            }
105
106            // mainly for CSV incoming data where everything is mapped to String
107            if (String.class.isInstance(value)) {
108                return mapString(expectedType, String.valueOf(value));
109            }
110
111            throw new IllegalArgumentException(String
112                    .format("%s can't be converted to %s as its value is '%s' of type %s.", name, expectedType, value,
113                            value.getClass()));
114        }
115        // type should match so...
116        return value;
117    }
118
119    public static <T> Object mapPrimitiveWrapper(final Class<T> expected, final Object value) {
120        if (char.class == expected || Character.class == expected) {
121            return expected.isPrimitive() ? Character.class.cast(value).charValue() : value;
122        }
123        if (Boolean.class == expected || boolean.class == expected) {
124            return expected.isPrimitive() ? Boolean.class.cast(value).booleanValue() : value;
125        }
126        if (Integer.class == expected || int.class == expected) {
127            return expected.isPrimitive() ? Integer.class.cast(value).intValue() : value;
128        }
129        if (Long.class == expected || long.class == expected) {
130            return expected.isPrimitive() ? Long.class.cast(value).longValue() : value;
131        }
132        if (Short.class == expected || short.class == expected) {
133            return expected.isPrimitive() ? Short.class.cast(value).shortValue() : value;
134        }
135        if (Byte.class == expected || byte.class == expected) {
136            return expected.isPrimitive() ? Byte.class.cast(value).byteValue() : value;
137        }
138        if (Float.class == expected || float.class == expected) {
139            return expected.isPrimitive() ? Float.class.cast(value).floatValue() : value;
140        }
141        if (Double.class == expected || double.class == expected) {
142            return expected.isPrimitive() ? Double.class.cast(value).doubleValue() : value;
143        }
144
145        throw new IllegalArgumentException(String.format("Can't convert %s to %s.", value, expected));
146    }
147
148    public static <T> Object mapNumber(final Class<T> expected, final Number value) {
149        if (expected == BigDecimal.class) {
150            return BigDecimal.valueOf(value.doubleValue());
151        }
152        if (expected == Double.class || expected == double.class) {
153            return value.doubleValue();
154        }
155        if (expected == Float.class || expected == float.class) {
156            return value.floatValue();
157        }
158        if (expected == Integer.class || expected == int.class) {
159            return value.intValue();
160        }
161        if (expected == Long.class || expected == long.class) {
162            return value.longValue();
163        }
164        if (expected == Byte.class || expected == byte.class) {
165            return value.byteValue();
166        }
167        if (expected == Short.class || expected == short.class) {
168            return value.shortValue();
169        }
170
171        throw new IllegalArgumentException(String.format("Can't convert %s to %s.", value, expected));
172    }
173
174    public static <T> Object mapString(final Class<T> expected, final String value) {
175        if (Boolean.class == expected || boolean.class == expected) {
176            return Boolean.valueOf(value);
177        }
178        // handle null literal
179        if ("null".equalsIgnoreCase(value.trim())) {
180            return null;
181        }
182        //
183        final boolean isNumeric = value.chars().allMatch(Character::isDigit);
184        if (ZonedDateTime.class == expected) {
185            if (isNumeric) {
186                return ZonedDateTime.ofInstant(Instant.ofEpochMilli(Long.valueOf(value)), UTC);
187            } else {
188                return ZonedDateTime.parse(value);
189            }
190        }
191        if (Date.class == expected) {
192            if (isNumeric) {
193                return Date.from(Instant.ofEpochMilli(Long.valueOf(value)));
194
195            } else {
196                return Date.from(ZonedDateTime.parse(value).toInstant());
197            }
198        }
199        if (char.class == expected || Character.class == expected) {
200            return value.isEmpty() ? Character.MIN_VALUE : value.charAt(0);
201        }
202        if (byte[].class == expected) {
203            log
204                    .warn("[mapString] Expecting a `byte[]` but received a `String`."
205                            + " Using `Base64.getDecoder().decode()` and "
206                            + "`String.getBytes()` if first fails: result may be inaccurate.");
207            // json is using Base64.getEncoder()
208            try {
209                return Base64.getDecoder().decode(value);
210            } catch (final Exception e) {
211                return value.getBytes();
212            }
213        }
214        if (BigDecimal.class == expected) {
215            return new BigDecimal(value);
216        }
217        if (Integer.class == expected || int.class == expected) {
218            return Integer.valueOf(value);
219        }
220        if (Long.class == expected || long.class == expected) {
221            return Long.valueOf(value);
222        }
223        if (Short.class == expected || short.class == expected) {
224            return Short.valueOf(value);
225        }
226        if (Byte.class == expected || byte.class == expected) {
227            return Byte.valueOf(value);
228        }
229        if (Float.class == expected || float.class == expected) {
230            return Float.valueOf(value);
231        }
232        if (Double.class == expected || double.class == expected) {
233            return Double.valueOf(value);
234        }
235
236        throw new IllegalArgumentException(String.format("Can't convert %s to %s.", value, expected));
237    }
238
239    public static boolean isPrimitiveWrapperOf(final Class<?> targetClass, final Class<?> primitive) {
240        if (!primitive.isPrimitive()) {
241            throw new IllegalArgumentException("First argument has to be primitive type");
242        }
243        return PRIMITIVE_WRAPPER_MAP.get(primitive) == targetClass;
244    }
245
246    public static boolean isAssignableTo(final Class<?> from, final Class<?> to) {
247        if (to.isAssignableFrom(from)) {
248            return true;
249        }
250        if (from.isPrimitive()) {
251            return isPrimitiveWrapperOf(to, from);
252        }
253        if (to.isPrimitive()) {
254            return isPrimitiveWrapperOf(from, to);
255        }
256        return false;
257    }
258
259}