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}