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.json; 017 018import static java.util.stream.Collectors.toList; 019 020import java.io.OutputStream; 021import java.io.Writer; 022import java.lang.reflect.Field; 023import java.math.BigDecimal; 024import java.math.BigInteger; 025import java.nio.charset.Charset; 026import java.nio.charset.StandardCharsets; 027import java.time.ZonedDateTime; 028import java.util.ArrayList; 029import java.util.Collection; 030import java.util.LinkedList; 031import java.util.List; 032import java.util.Map; 033import java.util.Objects; 034import java.util.function.Supplier; 035import java.util.stream.Collector; 036 037import javax.json.JsonArray; 038import javax.json.JsonNumber; 039import javax.json.JsonObject; 040import javax.json.JsonString; 041import javax.json.JsonValue; 042import javax.json.JsonValue.ValueType; 043import javax.json.bind.Jsonb; 044import javax.json.stream.JsonGenerator; 045import javax.json.stream.JsonGeneratorFactory; 046 047import org.talend.sdk.component.api.record.Record; 048import org.talend.sdk.component.api.record.Schema; 049import org.talend.sdk.component.api.record.Schema.Type; 050import org.talend.sdk.component.api.service.record.RecordBuilderFactory; 051import org.talend.sdk.component.runtime.record.RecordConverters; 052 053import lombok.RequiredArgsConstructor; 054 055@RequiredArgsConstructor 056public class RecordJsonGenerator implements JsonGenerator { 057 058 private final RecordBuilderFactory factory; 059 060 private final Jsonb jsonb; 061 062 private final OutputRecordHolder holder; 063 064 private final LinkedList<Object> builders = new LinkedList<>(); 065 066 private Record.Builder objectBuilder; 067 068 private Collection<Object> arrayBuilder; 069 070 private final RecordConverters recordConverters = new RecordConverters(); 071 072 private final RecordConverters.MappingMetaRegistry mappingRegistry = new RecordConverters.MappingMetaRegistry(); 073 074 private Field getField(final Class<?> clazz, final String fieldName) { 075 Class<?> tmpClass = clazz; 076 do { 077 try { 078 Field f = tmpClass.getDeclaredField(fieldName); 079 return f; 080 } catch (NoSuchFieldException e) { 081 tmpClass = tmpClass.getSuperclass(); 082 } 083 } while (tmpClass != null && tmpClass != Object.class); 084 085 return null; 086 } 087 088 @Override 089 public JsonGenerator writeStartObject() { 090 objectBuilder = factory.newRecordBuilder(); 091 builders.add(objectBuilder); 092 arrayBuilder = null; 093 return this; 094 } 095 096 @Override 097 public JsonGenerator writeStartObject(final String name) { 098 objectBuilder = factory.newRecordBuilder(); 099 if (holder.getData() != null) { 100 final Field f = getField(holder.getData().getClass(), name); 101 if (f != null) { 102 try { 103 f.setAccessible(true); 104 final Object o = f.get(holder.getData()); 105 final Record r = recordConverters.toRecord(mappingRegistry, o, () -> jsonb, () -> factory); 106 objectBuilder = factory.newRecordBuilder(r.getSchema(), r); 107 } catch (IllegalAccessException e) { 108 } 109 } 110 } 111 builders.add(new NamedBuilder<>(objectBuilder, name)); 112 arrayBuilder = null; 113 return this; 114 } 115 116 @Override 117 public JsonGenerator writeStartArray() { 118 arrayBuilder = new ArrayList<>(); 119 builders.add(arrayBuilder); 120 objectBuilder = null; 121 return this; 122 } 123 124 @Override 125 public JsonGenerator writeStartArray(final String name) { 126 arrayBuilder = new ArrayList<>(); 127 builders.add(new NamedBuilder<>(arrayBuilder, name)); 128 objectBuilder = null; 129 return this; 130 } 131 132 @Override 133 public JsonGenerator writeKey(final String name) { 134 throw new UnsupportedOperationException(); 135 } 136 137 @Override 138 public JsonGenerator write(final String name, final JsonValue value) { 139 switch (value.getValueType()) { 140 case ARRAY: 141 JsonValue jv = JsonValue.class.cast(Collection.class.cast(value).iterator().next()); 142 if (jv.getValueType().equals(ValueType.TRUE) || jv.getValueType().equals(ValueType.FALSE)) { 143 objectBuilder 144 .withArray( 145 factory 146 .newEntryBuilder() 147 .withName(name) 148 .withType(Type.ARRAY) 149 .withElementSchema(factory.newSchemaBuilder(Type.BOOLEAN).build()) 150 .build(), 151 Collection.class 152 .cast(Collection.class 153 .cast(value) 154 .stream() 155 .map(v -> JsonValue.class.cast(v) 156 .getValueType() 157 .equals(ValueType.TRUE)) 158 .collect(toList()))); 159 } else { 160 objectBuilder 161 .withArray(createEntryForJsonArray(name, Collection.class.cast(value)), 162 Collection.class.cast(value)); 163 } 164 break; 165 case OBJECT: 166 Record r = recordConverters.toRecord(mappingRegistry, value, () -> jsonb, () -> factory); 167 objectBuilder.withRecord(name, r); 168 break; 169 case STRING: 170 objectBuilder.withString(name, JsonString.class.cast(value).getString()); 171 break; 172 case NUMBER: 173 objectBuilder.withDouble(name, JsonNumber.class.cast(value).numberValue().doubleValue()); 174 break; 175 case TRUE: 176 objectBuilder.withBoolean(name, true); 177 break; 178 case FALSE: 179 objectBuilder.withBoolean(name, false); 180 break; 181 case NULL: 182 break; 183 default: 184 throw new IllegalStateException("Unexpected value: " + value.getValueType()); 185 } 186 return this; 187 } 188 189 @Override 190 public JsonGenerator write(final String name, final String value) { 191 objectBuilder.withString(name, value); 192 return this; 193 } 194 195 @Override 196 public JsonGenerator write(final String name, final BigInteger value) { 197 objectBuilder.withLong(name, value.longValue()); 198 return this; 199 } 200 201 @Override 202 public JsonGenerator write(final String name, final BigDecimal value) { 203 objectBuilder.withDouble(name, value.doubleValue()); 204 return this; 205 } 206 207 @Override 208 public JsonGenerator write(final String name, final int value) { 209 objectBuilder.withInt(name, value); 210 return this; 211 } 212 213 @Override 214 public JsonGenerator write(final String name, final long value) { 215 objectBuilder.withLong(name, value); 216 return this; 217 } 218 219 @Override 220 public JsonGenerator write(final String name, final double value) { 221 objectBuilder.withDouble(name, value); 222 return this; 223 } 224 225 @Override 226 public JsonGenerator write(final String name, final boolean value) { 227 objectBuilder.withBoolean(name, value); 228 return this; 229 } 230 231 @Override 232 public JsonGenerator writeNull(final String name) { 233 // skipped 234 return this; 235 } 236 237 @Override 238 public JsonGenerator write(final JsonValue value) { 239 switch (value.getValueType()) { 240 case ARRAY: 241 arrayBuilder.add(Collection.class.cast(value)); 242 break; 243 case OBJECT: 244 Record r = recordConverters.toRecord(mappingRegistry, value, () -> jsonb, () -> factory); 245 arrayBuilder.add(factory.newRecordBuilder(r.getSchema(), r)); 246 break; 247 case STRING: 248 arrayBuilder.add(JsonString.class.cast(value).getString()); 249 break; 250 case NUMBER: 251 arrayBuilder.add(JsonNumber.class.cast(value).numberValue().doubleValue()); 252 break; 253 case TRUE: 254 arrayBuilder.add(true); 255 break; 256 case FALSE: 257 arrayBuilder.add(false); 258 break; 259 case NULL: 260 break; 261 default: 262 throw new IllegalStateException("Unexpected value: " + value.getValueType()); 263 } 264 return this; 265 } 266 267 @Override 268 public JsonGenerator write(final String value) { 269 arrayBuilder.add(value); 270 return this; 271 } 272 273 @Override 274 public JsonGenerator write(final BigDecimal value) { 275 arrayBuilder.add(value); 276 return this; 277 } 278 279 @Override 280 public JsonGenerator write(final BigInteger value) { 281 arrayBuilder.add(value); 282 return this; 283 } 284 285 @Override 286 public JsonGenerator write(final int value) { 287 arrayBuilder.add(value); 288 return this; 289 } 290 291 @Override 292 public JsonGenerator write(final long value) { 293 arrayBuilder.add(value); 294 return this; 295 } 296 297 @Override 298 public JsonGenerator write(final double value) { 299 arrayBuilder.add(value); 300 return this; 301 } 302 303 @Override 304 public JsonGenerator write(final boolean value) { 305 arrayBuilder.add(value); 306 return this; 307 } 308 309 @Override 310 public JsonGenerator writeEnd() { 311 if (builders.size() == 1) { 312 return this; 313 } 314 315 final Object last = builders.removeLast(); 316 317 /* 318 * Previous potential cases: 319 * 1. json array -> we add the builder directly 320 * 2. NamedBuilder{array|object} -> we add the builder in the previous object 321 */ 322 323 final String name; 324 Object previous = builders.getLast(); 325 if (NamedBuilder.class.isInstance(previous)) { 326 final NamedBuilder namedBuilder = NamedBuilder.class.cast(previous); 327 name = namedBuilder.name; 328 previous = namedBuilder.builder; 329 } else { 330 name = null; 331 } 332 333 if (List.class.isInstance(last)) { 334 final List array = List.class.cast(last); 335 if (Collection.class.isInstance(previous)) { 336 arrayBuilder = Collection.class.cast(previous); 337 objectBuilder = null; 338 arrayBuilder.add(array); 339 } else if (Record.Builder.class.isInstance(previous)) { 340 objectBuilder = Record.Builder.class.cast(previous); 341 arrayBuilder = null; 342 objectBuilder.withArray(createEntryBuilderForArray(name, array).build(), prepareArray(array)); 343 } else { 344 throw new IllegalArgumentException("Unsupported previous builder: " + previous); 345 } 346 } else if (Record.Builder.class.isInstance(last)) { 347 final Record.Builder object = Record.Builder.class.cast(last); 348 if (Collection.class.isInstance(previous)) { 349 arrayBuilder = Collection.class.cast(previous); 350 objectBuilder = null; 351 arrayBuilder.add(object); 352 } else if (Record.Builder.class.isInstance(previous)) { 353 objectBuilder = Record.Builder.class.cast(previous); 354 arrayBuilder = null; 355 objectBuilder.withRecord(name, objectBuilder.build()); 356 } else { 357 throw new IllegalArgumentException("Unsupported previous builder: " + previous); 358 } 359 } else if (NamedBuilder.class.isInstance(last)) { 360 final NamedBuilder<?> namedBuilder = NamedBuilder.class.cast(last); 361 if (Record.Builder.class.isInstance(previous)) { 362 objectBuilder = Record.Builder.class.cast(previous); 363 if (List.class.isInstance(namedBuilder.builder)) { 364 final List array = List.class.cast(namedBuilder.builder); 365 objectBuilder 366 .withArray(createEntryBuilderForArray(namedBuilder.name, array).build(), 367 prepareArray(array)); 368 arrayBuilder = null; 369 } else if (Record.Builder.class.isInstance(namedBuilder.builder)) { 370 objectBuilder 371 .withRecord(namedBuilder.name, Record.Builder.class.cast(namedBuilder.builder).build()); 372 arrayBuilder = null; 373 } else { 374 throw new IllegalArgumentException("Unsupported previous builder: " + previous); 375 } 376 } else { 377 throw new IllegalArgumentException( 378 "Unsupported previous builder, expected object builder: " + previous); 379 } 380 } else { 381 throw new IllegalArgumentException("Unsupported previous builder: " + previous); 382 } 383 return this; 384 } 385 386 private List prepareArray(final List array) { 387 return ((Collection<?>) array) 388 .stream() 389 .map(it -> Record.Builder.class.isInstance(it) ? Record.Builder.class.cast(it).build() : it) 390 .collect(toList()); 391 } 392 393 private Schema.Entry createEntryForJsonArray(final String name, final Collection array) { 394 final Schema.Type type = findType(array); 395 final Schema.Entry.Builder builder = factory.newEntryBuilder().withName(name).withType(Schema.Type.ARRAY); 396 if (type == Schema.Type.RECORD) { 397 final JsonObject first = JsonObject.class.cast(array.iterator().next()); 398 final Schema.Builder rBuilder = first 399 .entrySet() 400 .stream() 401 .collect(Collector.of(() -> factory.newSchemaBuilder(Type.RECORD), (schemaBuilder, entry) -> { 402 final String k = entry.getKey(); 403 final JsonValue v = entry.getValue(); 404 schemaBuilder 405 .withEntry( 406 factory.newEntryBuilder().withName(k).withType(findType(v.getClass())).build()); 407 }, (b1, b2) -> { 408 throw new IllegalStateException(); 409 })); 410 builder.withElementSchema(rBuilder.build()); 411 } else { 412 builder.withElementSchema(factory.newSchemaBuilder(type).build()); 413 } 414 return builder.build(); 415 } 416 417 private Schema.Entry.Builder createEntryBuilderForArray(final String name, final List array) { 418 final Schema.Type type = findType(array); 419 final Schema.Entry.Builder builder = factory.newEntryBuilder().withName(name).withType(Schema.Type.ARRAY); 420 if (type == Schema.Type.RECORD) { 421 final Record first = Record.Builder.class.cast(array.iterator().next()).build(); 422 array.set(0, factory.newRecordBuilder(first.getSchema(), first)); // copy since build() resetted it 423 builder.withElementSchema(first.getSchema()); 424 } else { 425 builder.withElementSchema(factory.newSchemaBuilder(type).build()); 426 } 427 return builder; 428 } 429 430 private Schema.Type findType(final Collection<?> array) { 431 if (array.isEmpty()) { 432 return Schema.Type.STRING; 433 } 434 final Class<?> clazz = array.stream().filter(Objects::nonNull).findFirst().map(Object::getClass).orElse(null); 435 return findType(clazz); 436 } 437 438 private Schema.Type findType(final Class<?> clazz) { 439 if (clazz == null) { 440 return Schema.Type.STRING; 441 } 442 if (Collection.class.isAssignableFrom(clazz)) { 443 return Schema.Type.ARRAY; 444 } 445 if (CharSequence.class.isAssignableFrom(clazz)) { 446 return Schema.Type.STRING; 447 } 448 if (int.class == clazz || Integer.class == clazz) { 449 return Schema.Type.INT; 450 } 451 if (long.class == clazz || Long.class == clazz) { 452 return Schema.Type.LONG; 453 } 454 if (boolean.class == clazz || Boolean.class == clazz) { 455 return Schema.Type.BOOLEAN; 456 } 457 if (float.class == clazz || Float.class == clazz) { 458 return Schema.Type.FLOAT; 459 } 460 if (double.class == clazz || Double.class == clazz) { 461 return Schema.Type.DOUBLE; 462 } 463 if (byte[].class == clazz) { 464 return Schema.Type.BYTES; 465 } 466 if (ZonedDateTime.class == clazz) { 467 return Schema.Type.DATETIME; 468 } 469 if (BigDecimal.class == clazz) { 470 return Type.DECIMAL; 471 } 472 if (JsonArray.class.isAssignableFrom(clazz)) { 473 return Schema.Type.ARRAY; 474 } 475 if (JsonObject.class.isAssignableFrom(clazz)) { 476 return Schema.Type.RECORD; 477 } 478 if (JsonNumber.class.isAssignableFrom(clazz)) { 479 return Schema.Type.DOUBLE; 480 } 481 if (JsonString.class.isAssignableFrom(clazz)) { 482 return Schema.Type.STRING; 483 } 484 // JsonValue.TRUE or JsonValue.FALSE should not pass here, managed upstream. 485 if (JsonValue.class.isAssignableFrom(clazz)) { 486 return Schema.Type.STRING; 487 } 488 489 return Schema.Type.RECORD; 490 } 491 492 @Override 493 public JsonGenerator writeNull() { 494 // skipped 495 return this; 496 } 497 498 @Override 499 public void close() { 500 holder.setRecord(Record.Builder.class.cast(builders.getLast()).build()); 501 } 502 503 @Override 504 public void flush() { 505 // no-op 506 } 507 508 @RequiredArgsConstructor 509 public static class Factory implements JsonGeneratorFactory { 510 511 private final Supplier<RecordBuilderFactory> factory; 512 513 private final Supplier<Jsonb> jsonb; 514 515 private final Map<String, ?> configuration; 516 517 @Override 518 public JsonGenerator createGenerator(final Writer writer) { 519 if (OutputRecordHolder.class.isInstance(writer)) { 520 return new RecordJsonGenerator(factory.get(), jsonb.get(), OutputRecordHolder.class.cast(writer)); 521 } 522 throw new IllegalArgumentException("Unsupported writer: " + writer); 523 } 524 525 @Override 526 public JsonGenerator createGenerator(final OutputStream out) { 527 return createGenerator(out, StandardCharsets.UTF_8); 528 } 529 530 @Override 531 public JsonGenerator createGenerator(final OutputStream out, final Charset charset) { 532 throw new UnsupportedOperationException(); 533 } 534 535 @Override 536 public Map<String, ?> getConfigInUse() { 537 return configuration; 538 } 539 } 540 541 @RequiredArgsConstructor 542 private static class NamedBuilder<T> { 543 544 private final T builder; 545 546 private final String name; 547 } 548}