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.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}