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