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}