001/** 002 * Copyright (C) 2006-2024 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).getValueType().equals(ValueType.TRUE)) 156 .collect(toList()))); 157 } else { 158 objectBuilder 159 .withArray(createEntryForJsonArray(name, Collection.class.cast(value)), 160 Collection.class.cast(value)); 161 } 162 break; 163 case OBJECT: 164 Record r = recordConverters.toRecord(mappingRegistry, value, () -> jsonb, () -> factory); 165 objectBuilder.withRecord(name, r); 166 break; 167 case STRING: 168 objectBuilder.withString(name, JsonString.class.cast(value).getString()); 169 break; 170 case NUMBER: 171 objectBuilder.withDouble(name, JsonNumber.class.cast(value).numberValue().doubleValue()); 172 break; 173 case TRUE: 174 objectBuilder.withBoolean(name, true); 175 break; 176 case FALSE: 177 objectBuilder.withBoolean(name, false); 178 break; 179 case NULL: 180 break; 181 default: 182 throw new IllegalStateException("Unexpected value: " + value.getValueType()); 183 } 184 return this; 185 } 186 187 @Override 188 public JsonGenerator write(final String name, final String value) { 189 objectBuilder.withString(name, value); 190 return this; 191 } 192 193 @Override 194 public JsonGenerator write(final String name, final BigInteger value) { 195 objectBuilder.withLong(name, value.longValue()); 196 return this; 197 } 198 199 @Override 200 public JsonGenerator write(final String name, final BigDecimal value) { 201 objectBuilder.withDouble(name, value.doubleValue()); 202 return this; 203 } 204 205 @Override 206 public JsonGenerator write(final String name, final int value) { 207 objectBuilder.withInt(name, value); 208 return this; 209 } 210 211 @Override 212 public JsonGenerator write(final String name, final long value) { 213 objectBuilder.withLong(name, value); 214 return this; 215 } 216 217 @Override 218 public JsonGenerator write(final String name, final double value) { 219 objectBuilder.withDouble(name, value); 220 return this; 221 } 222 223 @Override 224 public JsonGenerator write(final String name, final boolean value) { 225 objectBuilder.withBoolean(name, value); 226 return this; 227 } 228 229 @Override 230 public JsonGenerator writeNull(final String name) { 231 // skipped 232 return this; 233 } 234 235 @Override 236 public JsonGenerator write(final JsonValue value) { 237 switch (value.getValueType()) { 238 case ARRAY: 239 arrayBuilder.add(Collection.class.cast(value)); 240 break; 241 case OBJECT: 242 Record r = recordConverters.toRecord(mappingRegistry, value, () -> jsonb, () -> factory); 243 arrayBuilder.add(factory.newRecordBuilder(r.getSchema(), r)); 244 break; 245 case STRING: 246 arrayBuilder.add(JsonString.class.cast(value).getString()); 247 break; 248 case NUMBER: 249 arrayBuilder.add(JsonNumber.class.cast(value).numberValue().doubleValue()); 250 break; 251 case TRUE: 252 arrayBuilder.add(true); 253 break; 254 case FALSE: 255 arrayBuilder.add(false); 256 break; 257 case NULL: 258 break; 259 default: 260 throw new IllegalStateException("Unexpected value: " + value.getValueType()); 261 } 262 return this; 263 } 264 265 @Override 266 public JsonGenerator write(final String value) { 267 arrayBuilder.add(value); 268 return this; 269 } 270 271 @Override 272 public JsonGenerator write(final BigDecimal value) { 273 arrayBuilder.add(value); 274 return this; 275 } 276 277 @Override 278 public JsonGenerator write(final BigInteger value) { 279 arrayBuilder.add(value); 280 return this; 281 } 282 283 @Override 284 public JsonGenerator write(final int value) { 285 arrayBuilder.add(value); 286 return this; 287 } 288 289 @Override 290 public JsonGenerator write(final long value) { 291 arrayBuilder.add(value); 292 return this; 293 } 294 295 @Override 296 public JsonGenerator write(final double value) { 297 arrayBuilder.add(value); 298 return this; 299 } 300 301 @Override 302 public JsonGenerator write(final boolean value) { 303 arrayBuilder.add(value); 304 return this; 305 } 306 307 @Override 308 public JsonGenerator writeEnd() { 309 if (builders.size() == 1) { 310 return this; 311 } 312 313 final Object last = builders.removeLast(); 314 315 /* 316 * Previous potential cases: 317 * 1. json array -> we add the builder directly 318 * 2. NamedBuilder{array|object} -> we add the builder in the previous object 319 */ 320 321 final String name; 322 Object previous = builders.getLast(); 323 if (NamedBuilder.class.isInstance(previous)) { 324 final NamedBuilder namedBuilder = NamedBuilder.class.cast(previous); 325 name = namedBuilder.name; 326 previous = namedBuilder.builder; 327 } else { 328 name = null; 329 } 330 331 if (List.class.isInstance(last)) { 332 final List array = List.class.cast(last); 333 if (Collection.class.isInstance(previous)) { 334 arrayBuilder = Collection.class.cast(previous); 335 objectBuilder = null; 336 arrayBuilder.add(array); 337 } else if (Record.Builder.class.isInstance(previous)) { 338 objectBuilder = Record.Builder.class.cast(previous); 339 arrayBuilder = null; 340 objectBuilder.withArray(createEntryBuilderForArray(name, array).build(), prepareArray(array)); 341 } else { 342 throw new IllegalArgumentException("Unsupported previous builder: " + previous); 343 } 344 } else if (Record.Builder.class.isInstance(last)) { 345 final Record.Builder object = Record.Builder.class.cast(last); 346 if (Collection.class.isInstance(previous)) { 347 arrayBuilder = Collection.class.cast(previous); 348 objectBuilder = null; 349 arrayBuilder.add(object); 350 } else if (Record.Builder.class.isInstance(previous)) { 351 objectBuilder = Record.Builder.class.cast(previous); 352 arrayBuilder = null; 353 objectBuilder.withRecord(name, objectBuilder.build()); 354 } else { 355 throw new IllegalArgumentException("Unsupported previous builder: " + previous); 356 } 357 } else if (NamedBuilder.class.isInstance(last)) { 358 final NamedBuilder<?> namedBuilder = NamedBuilder.class.cast(last); 359 if (Record.Builder.class.isInstance(previous)) { 360 objectBuilder = Record.Builder.class.cast(previous); 361 if (List.class.isInstance(namedBuilder.builder)) { 362 final List array = List.class.cast(namedBuilder.builder); 363 objectBuilder 364 .withArray(createEntryBuilderForArray(namedBuilder.name, array).build(), 365 prepareArray(array)); 366 arrayBuilder = null; 367 } else if (Record.Builder.class.isInstance(namedBuilder.builder)) { 368 objectBuilder 369 .withRecord(namedBuilder.name, Record.Builder.class.cast(namedBuilder.builder).build()); 370 arrayBuilder = null; 371 } else { 372 throw new IllegalArgumentException("Unsupported previous builder: " + previous); 373 } 374 } else { 375 throw new IllegalArgumentException( 376 "Unsupported previous builder, expected object builder: " + previous); 377 } 378 } else { 379 throw new IllegalArgumentException("Unsupported previous builder: " + previous); 380 } 381 return this; 382 } 383 384 private List prepareArray(final List array) { 385 return ((Collection<?>) array) 386 .stream() 387 .map(it -> Record.Builder.class.isInstance(it) ? Record.Builder.class.cast(it).build() : it) 388 .collect(toList()); 389 } 390 391 private Schema.Entry createEntryForJsonArray(final String name, final Collection array) { 392 final Schema.Type type = findType(array); 393 final Schema.Entry.Builder builder = factory.newEntryBuilder().withName(name).withType(Schema.Type.ARRAY); 394 if (type == Schema.Type.RECORD) { 395 final JsonObject first = JsonObject.class.cast(array.iterator().next()); 396 final Schema.Builder rBuilder = first 397 .entrySet() 398 .stream() 399 .collect(Collector.of(() -> factory.newSchemaBuilder(Type.RECORD), (schemaBuilder, entry) -> { 400 final String k = entry.getKey(); 401 final JsonValue v = entry.getValue(); 402 schemaBuilder 403 .withEntry( 404 factory.newEntryBuilder().withName(k).withType(findType(v.getClass())).build()); 405 }, (b1, b2) -> { 406 throw new IllegalStateException(); 407 })); 408 builder.withElementSchema(rBuilder.build()); 409 } else { 410 builder.withElementSchema(factory.newSchemaBuilder(type).build()); 411 } 412 return builder.build(); 413 } 414 415 private Schema.Entry.Builder createEntryBuilderForArray(final String name, final List array) { 416 final Schema.Type type = findType(array); 417 final Schema.Entry.Builder builder = factory.newEntryBuilder().withName(name).withType(Schema.Type.ARRAY); 418 if (type == Schema.Type.RECORD) { 419 final Record first = Record.Builder.class.cast(array.iterator().next()).build(); 420 array.set(0, factory.newRecordBuilder(first.getSchema(), first)); // copy since build() resetted it 421 builder.withElementSchema(first.getSchema()); 422 } else { 423 builder.withElementSchema(factory.newSchemaBuilder(type).build()); 424 } 425 return builder; 426 } 427 428 private Schema.Type findType(final Collection<?> array) { 429 if (array.isEmpty()) { 430 return Schema.Type.STRING; 431 } 432 final Class<?> clazz = array.stream().filter(Objects::nonNull).findFirst().map(Object::getClass).orElse(null); 433 return findType(clazz); 434 } 435 436 private Schema.Type findType(final Class<?> clazz) { 437 if (clazz == null) { 438 return Schema.Type.STRING; 439 } 440 if (Collection.class.isAssignableFrom(clazz)) { 441 return Schema.Type.ARRAY; 442 } 443 if (CharSequence.class.isAssignableFrom(clazz)) { 444 return Schema.Type.STRING; 445 } 446 if (int.class == clazz || Integer.class == clazz) { 447 return Schema.Type.INT; 448 } 449 if (long.class == clazz || Long.class == clazz) { 450 return Schema.Type.LONG; 451 } 452 if (boolean.class == clazz || Boolean.class == clazz) { 453 return Schema.Type.BOOLEAN; 454 } 455 if (float.class == clazz || Float.class == clazz) { 456 return Schema.Type.FLOAT; 457 } 458 if (double.class == clazz || Double.class == clazz) { 459 return Schema.Type.DOUBLE; 460 } 461 if (byte[].class == clazz) { 462 return Schema.Type.BYTES; 463 } 464 if (ZonedDateTime.class == clazz) { 465 return Schema.Type.DATETIME; 466 } 467 if (BigDecimal.class == clazz) { 468 return Type.DECIMAL; 469 } 470 if (JsonArray.class.isAssignableFrom(clazz)) { 471 return Schema.Type.ARRAY; 472 } 473 if (JsonObject.class.isAssignableFrom(clazz)) { 474 return Schema.Type.RECORD; 475 } 476 if (JsonNumber.class.isAssignableFrom(clazz)) { 477 return Schema.Type.DOUBLE; 478 } 479 if (JsonString.class.isAssignableFrom(clazz)) { 480 return Schema.Type.STRING; 481 } 482 // JsonValue.TRUE or JsonValue.FALSE should not pass here, managed upstream. 483 if (JsonValue.class.isAssignableFrom(clazz)) { 484 return Schema.Type.STRING; 485 } 486 487 return Schema.Type.RECORD; 488 } 489 490 @Override 491 public JsonGenerator writeNull() { 492 // skipped 493 return this; 494 } 495 496 @Override 497 public void close() { 498 holder.setRecord(Record.Builder.class.cast(builders.getLast()).build()); 499 } 500 501 @Override 502 public void flush() { 503 // no-op 504 } 505 506 @RequiredArgsConstructor 507 public static class Factory implements JsonGeneratorFactory { 508 509 private final Supplier<RecordBuilderFactory> factory; 510 511 private final Supplier<Jsonb> jsonb; 512 513 private final Map<String, ?> configuration; 514 515 @Override 516 public JsonGenerator createGenerator(final Writer writer) { 517 if (OutputRecordHolder.class.isInstance(writer)) { 518 return new RecordJsonGenerator(factory.get(), jsonb.get(), OutputRecordHolder.class.cast(writer)); 519 } 520 throw new IllegalArgumentException("Unsupported writer: " + writer); 521 } 522 523 @Override 524 public JsonGenerator createGenerator(final OutputStream out) { 525 return createGenerator(out, StandardCharsets.UTF_8); 526 } 527 528 @Override 529 public JsonGenerator createGenerator(final OutputStream out, final Charset charset) { 530 throw new UnsupportedOperationException(); 531 } 532 533 @Override 534 public Map<String, ?> getConfigInUse() { 535 return configuration; 536 } 537 } 538 539 @RequiredArgsConstructor 540 private static class NamedBuilder<T> { 541 542 private final T builder; 543 544 private final String name; 545 } 546}