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}