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 static java.util.stream.Collectors.toList; 019import static java.util.stream.Collectors.toSet; 020 021import java.io.ObjectStreamException; 022import java.io.Serializable; 023import java.lang.reflect.Constructor; 024import java.lang.reflect.InvocationTargetException; 025import java.lang.reflect.Method; 026import java.math.BigDecimal; 027import java.time.ZonedDateTime; 028import java.time.format.DateTimeFormatter; 029import java.util.Base64; 030import java.util.Collection; 031import java.util.Collections; 032import java.util.Date; 033import java.util.List; 034import java.util.Map; 035import java.util.Set; 036import java.util.concurrent.ConcurrentHashMap; 037import java.util.function.Function; 038import java.util.function.Supplier; 039import java.util.stream.Collector; 040import java.util.stream.Stream; 041 042import javax.json.JsonArray; 043import javax.json.JsonArrayBuilder; 044import javax.json.JsonBuilderFactory; 045import javax.json.JsonNumber; 046import javax.json.JsonObject; 047import javax.json.JsonObjectBuilder; 048import javax.json.JsonString; 049import javax.json.JsonValue; 050import javax.json.bind.Jsonb; 051import javax.json.spi.JsonProvider; 052 053import org.apache.johnzon.core.JsonLongImpl; 054import org.apache.johnzon.jsonb.extension.JsonValueReader; 055import org.talend.sdk.component.api.record.Record; 056import org.talend.sdk.component.api.record.Schema; 057import org.talend.sdk.component.api.service.record.RecordBuilderFactory; 058import org.talend.sdk.component.runtime.record.json.OutputRecordHolder; 059import org.talend.sdk.component.runtime.record.json.PojoJsonbProvider; 060 061import lombok.Data; 062 063public class RecordConverters implements Serializable { 064 065 public <T> Record toRecord(final MappingMetaRegistry registry, final T data, final Supplier<Jsonb> jsonbProvider, 066 final Supplier<RecordBuilderFactory> recordBuilderProvider) { 067 if (data == null) { 068 return null; 069 } 070 if (Record.class.isInstance(data)) { 071 return Record.class.cast(data); 072 } 073 if (JsonObject.class.isInstance(data)) { 074 return json2Record(recordBuilderProvider.get(), JsonObject.class.cast(data)); 075 } 076 077 final MappingMeta meta = registry.find(data.getClass()); 078 if (meta.isLinearMapping()) { 079 return meta.newRecord(data, recordBuilderProvider.get()); 080 } 081 082 final Jsonb jsonb = jsonbProvider.get(); 083 if (!String.class.isInstance(data) && !data.getClass().isPrimitive() 084 && PojoJsonbProvider.class.isInstance(jsonb)) { 085 final Jsonb pojoMapper = PojoJsonbProvider.class.cast(jsonb).get(); 086 final OutputRecordHolder holder = new OutputRecordHolder(data); 087 try (final OutputRecordHolder stream = holder) { 088 pojoMapper.toJson(data, stream); 089 } 090 return holder.getRecord(); 091 } 092 return json2Record(recordBuilderProvider.get(), jsonb.fromJson(jsonb.toJson(data), JsonObject.class)); 093 } 094 095 private Record json2Record(final RecordBuilderFactory factory, final JsonObject object) { 096 final Record.Builder builder = factory.newRecordBuilder(); 097 object.forEach((key, value) -> { 098 switch (value.getValueType()) { 099 case ARRAY: { 100 final List<Object> items = 101 value.asJsonArray().stream().map(it -> mapJson(factory, it)).collect(toList()); 102 builder 103 .withArray(factory 104 .newEntryBuilder() 105 .withName(key) 106 .withType(Schema.Type.ARRAY) 107 .withElementSchema(getArrayElementSchema(factory, items)) 108 .build(), items); 109 break; 110 } 111 case OBJECT: { 112 final Record record = json2Record(factory, value.asJsonObject()); 113 builder 114 .withRecord(factory 115 .newEntryBuilder() 116 .withName(key) 117 .withType(Schema.Type.RECORD) 118 .withElementSchema(record.getSchema()) 119 .build(), record); 120 break; 121 } 122 case TRUE: 123 case FALSE: 124 builder.withBoolean(key, JsonValue.TRUE.equals(value)); 125 break; 126 case STRING: 127 builder.withString(key, JsonString.class.cast(value).getString()); 128 break; 129 case NUMBER: 130 final JsonNumber number = JsonNumber.class.cast(value); 131 builder.withDouble(key, number.doubleValue()); 132 break; 133 case NULL: 134 break; 135 default: 136 throw new IllegalArgumentException("Unsupported value type: " + value); 137 } 138 }); 139 return builder.build(); 140 } 141 142 private Schema getArrayElementSchema(final RecordBuilderFactory factory, final List<Object> items) { 143 if (items.isEmpty()) { 144 return factory.newSchemaBuilder(Schema.Type.STRING).build(); 145 } 146 final Schema firstSchema = toSchema(factory, items.iterator().next()); 147 switch (firstSchema.getType()) { 148 case RECORD: 149 return items.stream().map(it -> toSchema(factory, it)).reduce(null, (s1, s2) -> { 150 if (s1 == null) { 151 return s2; 152 } 153 if (s2 == null) { // unlikely 154 return s1; 155 } 156 final Set<String> names1 = s1.getAllEntries().map(Schema.Entry::getName).collect(toSet()); 157 final Set<String> names2 = s2.getAllEntries().map(Schema.Entry::getName).collect(toSet()); 158 if (!names1.equals(names2)) { 159 // here we are not good since values will not be right anymore, 160 // forbidden for current version anyway but potentially supported later 161 final Schema.Builder builder = factory.newSchemaBuilder(Schema.Type.RECORD); 162 s1.getAllEntries().forEach(builder::withEntry); 163 s2.getAllEntries().filter(it -> !(names1.contains(it.getName()))).forEach(builder::withEntry); 164 return builder.build(); 165 } 166 return s1; 167 }); 168 default: 169 return firstSchema; 170 } 171 } 172 173 private Object mapJson(final RecordBuilderFactory factory, final JsonValue it) { 174 if (JsonObject.class.isInstance(it)) { 175 return json2Record(factory, JsonObject.class.cast(it)); 176 } 177 if (JsonArray.class.isInstance(it)) { 178 return JsonArray.class.cast(it).stream().map(i -> mapJson(factory, i)).collect(toList()); 179 } 180 if (JsonString.class.isInstance(it)) { 181 return JsonString.class.cast(it).getString(); 182 } 183 if (JsonNumber.class.isInstance(it)) { 184 return JsonNumber.class.cast(it).numberValue(); 185 } 186 if (JsonValue.FALSE.equals(it)) { 187 return false; 188 } 189 if (JsonValue.TRUE.equals(it)) { 190 return true; 191 } 192 if (JsonValue.NULL.equals(it)) { 193 return null; 194 } 195 return it; 196 } 197 198 public static Schema toSchema(final RecordBuilderFactory factory, final Object next) { 199 if (String.class.isInstance(next) || JsonString.class.isInstance(next)) { 200 return factory.newSchemaBuilder(Schema.Type.STRING).build(); 201 } 202 if (Integer.class.isInstance(next)) { 203 return factory.newSchemaBuilder(Schema.Type.INT).build(); 204 } 205 if (Long.class.isInstance(next) || JsonLongImpl.class.isInstance(next)) { 206 return factory.newSchemaBuilder(Schema.Type.LONG).build(); 207 } 208 if (Float.class.isInstance(next)) { 209 return factory.newSchemaBuilder(Schema.Type.FLOAT).build(); 210 } 211 if (JsonNumber.class.isInstance(next)) { 212 return factory.newSchemaBuilder(Schema.Type.DOUBLE).build(); 213 } 214 if (Double.class.isInstance(next) || JsonNumber.class.isInstance(next)) { 215 return factory.newSchemaBuilder(Schema.Type.DOUBLE).build(); 216 } 217 if (Boolean.class.isInstance(next) || JsonValue.TRUE.equals(next) || JsonValue.FALSE.equals(next)) { 218 return factory.newSchemaBuilder(Schema.Type.BOOLEAN).build(); 219 } 220 if (Date.class.isInstance(next) || ZonedDateTime.class.isInstance(next)) { 221 return factory.newSchemaBuilder(Schema.Type.DATETIME).build(); 222 } 223 if (BigDecimal.class.isInstance(next)) { 224 return factory.newSchemaBuilder(Schema.Type.DECIMAL).build(); 225 } 226 if (byte[].class.isInstance(next)) { 227 return factory.newSchemaBuilder(Schema.Type.BYTES).build(); 228 } 229 if (Collection.class.isInstance(next) || JsonArray.class.isInstance(next)) { 230 final Collection collection = Collection.class.cast(next); 231 if (collection.isEmpty()) { 232 return factory.newSchemaBuilder(Schema.Type.STRING).build(); 233 } 234 return factory 235 .newSchemaBuilder(Schema.Type.ARRAY) 236 .withElementSchema(toSchema(factory, collection.iterator().next())) 237 .build(); 238 } 239 if (Record.class.isInstance(next)) { 240 return Record.class.cast(next).getSchema(); 241 } 242 throw new IllegalArgumentException("unsupported type for " + next); 243 } 244 245 public Object toType(final MappingMetaRegistry registry, final Object data, final Class<?> parameterType, 246 final Supplier<JsonBuilderFactory> factorySupplier, final Supplier<JsonProvider> providerSupplier, 247 final Supplier<Jsonb> jsonbProvider, final Supplier<RecordBuilderFactory> recordBuilderProvider) { 248 return toType(registry, data, parameterType, factorySupplier, providerSupplier, jsonbProvider, 249 recordBuilderProvider, Collections.emptyMap()); 250 } 251 252 public Object toType(final MappingMetaRegistry registry, final Object data, final Class<?> parameterType, 253 final Supplier<JsonBuilderFactory> factorySupplier, final Supplier<JsonProvider> providerSupplier, 254 final Supplier<Jsonb> jsonbProvider, final Supplier<RecordBuilderFactory> recordBuilderProvider, 255 final java.util.Map<String, String> metadata) { 256 if (parameterType.isInstance(data)) { 257 return data; 258 } 259 260 final JsonObject inputAsJson; 261 if (JsonObject.class.isInstance(data)) { 262 if (JsonObject.class == parameterType) { 263 return data; 264 } 265 inputAsJson = JsonObject.class.cast(data); 266 } else if (Record.class.isInstance(data)) { 267 final Record record = Record.class.cast(data); 268 if (!JsonObject.class.isAssignableFrom(parameterType)) { 269 final MappingMeta mappingMeta = registry.find(parameterType); 270 if (mappingMeta.isLinearMapping()) { 271 return mappingMeta.newInstance(record, metadata); 272 } 273 } 274 final JsonObject asJson = toJson(factorySupplier, providerSupplier, record); 275 if (JsonObject.class == parameterType) { 276 return asJson; 277 } 278 inputAsJson = asJson; 279 } else { 280 if (parameterType == Record.class) { 281 return toRecord(registry, data, jsonbProvider, recordBuilderProvider); 282 } 283 final Jsonb jsonb = jsonbProvider.get(); 284 inputAsJson = jsonb.fromJson(jsonb.toJson(data), JsonObject.class); 285 } 286 return jsonbProvider.get().fromJson(new JsonValueReader<>(inputAsJson), parameterType); 287 } 288 289 private JsonObject toJson(final Supplier<JsonBuilderFactory> factorySupplier, 290 final Supplier<JsonProvider> providerSupplier, final Record record) { 291 return buildRecord(factorySupplier.get(), providerSupplier, record).build(); 292 } 293 294 private JsonObjectBuilder buildRecord(final JsonBuilderFactory factory, 295 final Supplier<JsonProvider> providerSupplier, final Record record) { 296 final Schema schema = record.getSchema(); 297 final JsonObjectBuilder builder = factory.createObjectBuilder(); 298 schema.getEntries().forEach(entry -> { 299 final String name = entry.getName(); 300 switch (entry.getType()) { 301 case STRING: { 302 final String value = record.get(String.class, name); 303 if (value != null) { 304 builder.add(name, value); 305 } 306 break; 307 } 308 case INT: { 309 final Integer value = record.get(Integer.class, name); 310 if (value != null) { 311 builder.add(name, value); 312 } 313 break; 314 } 315 case LONG: { 316 final Long value = record.get(Long.class, name); 317 if (value != null) { 318 builder.add(name, value); 319 } 320 break; 321 } 322 case FLOAT: { 323 final Float value = record.get(Float.class, name); 324 if (value != null) { 325 builder.add(name, value); 326 } 327 break; 328 } 329 case DOUBLE: { 330 final Double value = record.get(Double.class, name); 331 if (value != null) { 332 builder.add(name, value); 333 } 334 break; 335 } 336 case BOOLEAN: { 337 final Boolean value = record.get(Boolean.class, name); 338 if (value != null) { 339 builder.add(name, value); 340 } 341 break; 342 } 343 case BYTES: { 344 final byte[] value = record.get(byte[].class, name); 345 if (value != null) { 346 builder.add(name, Base64.getEncoder().encodeToString(value)); 347 } 348 break; 349 } 350 case DATETIME: { 351 final ZonedDateTime value = record.get(ZonedDateTime.class, name); 352 if (value != null) { 353 builder.add(name, value.format(DateTimeFormatter.ISO_ZONED_DATE_TIME)); 354 } 355 break; 356 } 357 case DECIMAL: { 358 final BigDecimal value = record.get(BigDecimal.class, name); 359 if (value != null) { 360 builder.add(name, value.toString()); 361 } 362 break; 363 } 364 case RECORD: { 365 final Record value = record.get(Record.class, name); 366 if (value != null) { 367 builder.add(name, buildRecord(factory, providerSupplier, value)); 368 } 369 break; 370 } 371 case ARRAY: 372 final Collection<?> collection = record.get(Collection.class, name); 373 if (collection == null) { 374 break; 375 } 376 if (collection.isEmpty()) { 377 builder.add(name, factory.createArrayBuilder().build()); 378 } else { // only homogeneous collections 379 final Object item = collection.iterator().next(); 380 if (String.class.isInstance(item)) { 381 final JsonProvider jsonProvider = providerSupplier.get(); 382 builder 383 .add(name, toArray(factory, v -> jsonProvider.createValue(String.class.cast(v)), 384 collection)); 385 } else if (Double.class.isInstance(item)) { 386 final JsonProvider jsonProvider = providerSupplier.get(); 387 builder 388 .add(name, toArray(factory, v -> jsonProvider.createValue(Double.class.cast(v)), 389 collection)); 390 } else if (Float.class.isInstance(item)) { 391 final JsonProvider jsonProvider = providerSupplier.get(); 392 builder 393 .add(name, toArray(factory, v -> jsonProvider.createValue(Float.class.cast(v)), 394 collection)); 395 } else if (Integer.class.isInstance(item)) { 396 final JsonProvider jsonProvider = providerSupplier.get(); 397 builder 398 .add(name, toArray(factory, v -> jsonProvider.createValue(Integer.class.cast(v)), 399 collection)); 400 } else if (Long.class.isInstance(item)) { 401 final JsonProvider jsonProvider = providerSupplier.get(); 402 builder 403 .add(name, toArray(factory, v -> jsonProvider.createValue(Long.class.cast(v)), 404 collection)); 405 } else if (Boolean.class.isInstance(item)) { 406 builder 407 .add(name, toArray(factory, 408 v -> Boolean.class.cast(v) ? JsonValue.TRUE : JsonValue.FALSE, collection)); 409 } else if (ZonedDateTime.class.isInstance(item)) { 410 final JsonProvider jsonProvider = providerSupplier.get(); 411 builder 412 .add(name, 413 toArray(factory, v -> jsonProvider 414 .createValue(ZonedDateTime.class.cast(v).toInstant().toEpochMilli()), 415 collection)); 416 } else if (Date.class.isInstance(item)) { 417 final JsonProvider jsonProvider = providerSupplier.get(); 418 builder 419 .add(name, toArray(factory, v -> jsonProvider.createValue(Date.class.cast(v).getTime()), 420 collection)); 421 } else if (Record.class.isInstance(item)) { 422 builder 423 .add(name, toArray(factory, 424 v -> buildRecord(factory, providerSupplier, Record.class.cast(v)).build(), 425 collection)); 426 } else if (JsonValue.class.isInstance(item)) { 427 builder.add(name, toArray(factory, JsonValue.class::cast, collection)); 428 } // else throw? 429 } 430 break; 431 default: 432 throw new IllegalArgumentException("Unsupported type: " + entry.getType() + " for '" + name + "'"); 433 } 434 }); 435 return builder; 436 } 437 438 private JsonArray toArray(final JsonBuilderFactory factory, final Function<Object, JsonValue> valueFactory, 439 final Collection<?> collection) { 440 final Collector<JsonValue, JsonArrayBuilder, JsonArray> collector = Collector 441 .of(factory::createArrayBuilder, JsonArrayBuilder::add, JsonArrayBuilder::addAll, 442 JsonArrayBuilder::build); 443 return collection.stream().map(valueFactory).collect(collector); 444 } 445 446 public <T> T coerce(final Class<T> expectedType, final Object value, final String name) { 447 if (value == null) { 448 return null; 449 } 450 451 // here mean get(Object.class, name) return origin store type, like DATETIME return long, is expected? 452 if (!expectedType.isInstance(value)) { 453 return expectedType.cast(MappingUtils.coerce(expectedType, value, name)); 454 } 455 456 return expectedType.cast(value); 457 } 458 459 @Data 460 public static class MappingMeta { 461 462 private final boolean linearMapping; 463 464 private final Class<?> rowStruct; 465 466 private Object recordVisitor; 467 468 private Method visitRecord; 469 470 private Object rowStructVisitor; 471 472 private Method visitRowStruct; 473 474 public MappingMeta(final Class<?> type, final MappingMetaRegistry registry) { 475 linearMapping = Stream.of(type.getInterfaces()).anyMatch(it -> it.getName().startsWith("routines.system.")); 476 rowStruct = type; 477 } 478 479 public Object newInstance(final Record record) { 480 return newInstance(record, Collections.emptyMap()); 481 } 482 483 public Object newInstance(final Record record, final java.util.Map<String, String> metadata) { 484 if (recordVisitor == null) { 485 try { 486 final String className = "org.talend.sdk.component.runtime.di.record.DiRecordVisitor"; 487 final Class<?> visitorClass = getClass().getClassLoader().loadClass(className); 488 final Constructor<?> constructor = visitorClass.getDeclaredConstructors()[0]; 489 constructor.setAccessible(true); 490 recordVisitor = constructor.newInstance(rowStruct, metadata); 491 visitRecord = visitorClass.getDeclaredMethod("visit", Record.class); 492 } catch (final ClassNotFoundException | InstantiationException | IllegalAccessException 493 | InvocationTargetException | NoSuchMethodException e) { 494 throw new IllegalStateException(e); 495 } 496 } 497 try { 498 return visitRecord.invoke(recordVisitor, record); 499 } catch (final IllegalAccessException | InvocationTargetException e) { 500 throw new IllegalStateException(e); 501 } 502 } 503 504 public <T> Record newRecord(final T data, final RecordBuilderFactory factory) { 505 if (rowStructVisitor == null) { 506 try { 507 final String className = "org.talend.sdk.component.runtime.di.record.DiRowStructVisitor"; 508 final Class<?> visitorClass = getClass().getClassLoader().loadClass(className); 509 final Constructor<?> constructor = visitorClass.getConstructors()[0]; 510 constructor.setAccessible(true); 511 rowStructVisitor = constructor.newInstance(); 512 visitRowStruct = visitorClass.getDeclaredMethod("get", Object.class, RecordBuilderFactory.class); 513 } catch (final ClassNotFoundException | InstantiationException | IllegalAccessException 514 | InvocationTargetException | NoSuchMethodException e) { 515 throw new IllegalStateException(e); 516 } 517 } 518 try { 519 return Record.class.cast(visitRowStruct.invoke(rowStructVisitor, data, factory)); 520 } catch (final IllegalAccessException | InvocationTargetException e) { 521 throw new IllegalStateException(e); 522 } 523 } 524 525 } 526 527 @Data 528 public static class MappingMetaRegistry implements Serializable { 529 530 protected final Map<Class<?>, MappingMeta> registry = new ConcurrentHashMap<>(); 531 532 private Object writeReplace() throws ObjectStreamException { 533 return new Factory(); // don't serialize the mapping, recalculate it lazily 534 } 535 536 public MappingMeta find(final Class<?> parameterType) { 537 final MappingMeta meta = registry.get(parameterType); 538 if (meta != null) { 539 return meta; 540 } 541 final MappingMeta mappingMeta = new MappingMeta(parameterType, this); 542 final MappingMeta existing = registry.putIfAbsent(parameterType, mappingMeta); 543 if (existing != null) { 544 return existing; 545 } 546 return mappingMeta; 547 } 548 549 public static class Factory implements Serializable { 550 551 private Object readResolve() throws ObjectStreamException { 552 return new MappingMetaRegistry(); 553 } 554 } 555 } 556}