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.manager.service;
017
018import java.io.Serializable;
019import java.math.BigDecimal;
020import java.time.ZonedDateTime;
021import java.util.Collection;
022import java.util.Optional;
023import java.util.OptionalDouble;
024import java.util.OptionalInt;
025import java.util.OptionalLong;
026import java.util.concurrent.atomic.AtomicBoolean;
027import java.util.concurrent.atomic.AtomicReference;
028import java.util.function.BiConsumer;
029import java.util.function.BiFunction;
030import java.util.function.Supplier;
031import java.util.stream.Collector;
032
033import javax.json.JsonBuilderFactory;
034import javax.json.bind.Jsonb;
035import javax.json.spi.JsonProvider;
036
037import org.talend.sdk.component.api.record.Record;
038import org.talend.sdk.component.api.record.Schema;
039import org.talend.sdk.component.api.record.SchemaProperty;
040import org.talend.sdk.component.api.service.record.RecordBuilderFactory;
041import org.talend.sdk.component.api.service.record.RecordService;
042import org.talend.sdk.component.api.service.record.RecordVisitor;
043import org.talend.sdk.component.runtime.record.RecordConverters;
044import org.talend.sdk.component.runtime.serialization.SerializableService;
045
046import lombok.Data;
047
048@Data
049public class RecordServiceImpl implements RecordService, Serializable {
050
051    private final String plugin;
052
053    private final RecordBuilderFactory recordBuilderFactory;
054
055    private final Supplier<JsonBuilderFactory> jsonBuilderFactorySupplier;
056
057    private final Supplier<JsonProvider> jsonProvider;
058
059    private final Supplier<Jsonb> jsonbSupplier;
060
061    private final RecordConverters recordConverters = new RecordConverters();
062
063    private final RecordConverters.MappingMetaRegistry mappingRegistry = new RecordConverters.MappingMetaRegistry();
064
065    @Override
066    public Collector<Schema.Entry, Record.Builder, Record> toRecord(final Schema schema, final Record fallbackRecord,
067            final BiFunction<Schema.Entry, Record.Builder, Boolean> customHandler,
068            final BiConsumer<Record.Builder, Boolean> beforeFinish) {
069        final AtomicBoolean customHandlerCalled = new AtomicBoolean();
070        return Collector.of(() -> recordBuilderFactory.newRecordBuilder(schema), (builder, entry) -> {
071            if (!customHandler.apply(entry, builder)) {
072                forwardEntry(fallbackRecord, builder, entry.getName(), entry);
073            } else {
074                customHandlerCalled.set(true);
075            }
076        }, (b1, b2) -> {
077            throw new IllegalStateException("merge unsupported");
078        }, builder -> {
079            beforeFinish.accept(builder, customHandlerCalled.get());
080            return builder.build();
081        });
082    }
083
084    @Override
085    public Record create(final Schema schema, final Record fallbackRecord,
086            final BiFunction<Schema.Entry, Record.Builder, Boolean> customHandler,
087            final BiConsumer<Record.Builder, Boolean> beforeFinish) {
088        return fallbackRecord
089                .getSchema()
090                .getAllEntries()
091                .collect(toRecord(schema, fallbackRecord, customHandler, beforeFinish));
092    }
093
094    @Override
095    public <T> T visit(final RecordVisitor<T> visitor, final Record record) {
096        final AtomicReference<T> out = new AtomicReference<>();
097        record.getSchema().getAllEntries().forEach(entry -> {
098            switch (entry.getType()) {
099                case INT:
100                    visitor.onInt(entry, record.getOptionalInt(entry.getName()));
101                    break;
102                case LONG:
103                    visitor.onLong(entry, record.getOptionalLong(entry.getName()));
104                    break;
105                case FLOAT:
106                    visitor.onFloat(entry, record.getOptionalFloat(entry.getName()));
107                    break;
108                case DOUBLE:
109                    visitor.onDouble(entry, record.getOptionalDouble(entry.getName()));
110                    break;
111                case BOOLEAN:
112                    visitor.onBoolean(entry, record.getOptionalBoolean(entry.getName()));
113                    break;
114                case STRING:
115                    String insideType = entry.getProp(SchemaProperty.STUDIO_TYPE);
116                    if ("id_Object".equals(insideType)) {
117                        visitor.onObject(entry, Optional.ofNullable(record.get(Object.class, entry.getName())));
118                    } else {
119                        visitor.onString(entry, record.getOptionalString(entry.getName()));
120                    }
121                    break;
122                case DATETIME:
123                    visitor.onDatetime(entry, record.getOptionalDateTime(entry.getName()));
124                    break;
125                case DECIMAL:
126                    visitor.onDecimal(entry, record.getOptionalDecimal(entry.getName()));
127                    break;
128                case BYTES:
129                    visitor.onBytes(entry, record.getOptionalBytes(entry.getName()));
130                    break;
131                case RECORD:
132                    final Optional<Record> optionalRecord = record.getOptionalRecord(entry.getName());
133                    final RecordVisitor<T> recordVisitor = visitor.onRecord(entry, optionalRecord);
134                    if (recordVisitor != null) {
135                        optionalRecord.ifPresent(r -> {
136                            final T visited = visit(recordVisitor, r);
137                            if (visited != null) {
138                                final T current = out.get();
139                                out.set(current == null ? visited : visitor.apply(current, visited));
140                            }
141                        });
142                    }
143                    break;
144                case ARRAY:
145                    final Schema schema = entry.getElementSchema();
146                    switch (schema.getType()) {
147                        case INT:
148                            visitor.onIntArray(entry, record.getOptionalArray(int.class, entry.getName()));
149                            break;
150                        case LONG:
151                            visitor.onLongArray(entry, record.getOptionalArray(long.class, entry.getName()));
152                            break;
153                        case FLOAT:
154                            visitor.onFloatArray(entry, record.getOptionalArray(float.class, entry.getName()));
155                            break;
156                        case DOUBLE:
157                            visitor.onDoubleArray(entry, record.getOptionalArray(double.class, entry.getName()));
158                            break;
159                        case BOOLEAN:
160                            visitor.onBooleanArray(entry, record.getOptionalArray(boolean.class, entry.getName()));
161                            break;
162                        case STRING:
163                            visitor.onStringArray(entry, record.getOptionalArray(String.class, entry.getName()));
164                            break;
165                        case DATETIME:
166                            visitor.onDatetimeArray(entry,
167                                    record.getOptionalArray(ZonedDateTime.class, entry.getName()));
168                            break;
169                        case DECIMAL:
170                            visitor.onDecimalArray(entry, record.getOptionalArray(BigDecimal.class, entry.getName()));
171                            break;
172                        case BYTES:
173                            visitor.onBytesArray(entry, record.getOptionalArray(byte[].class, entry.getName()));
174                            break;
175                        case RECORD:
176                            final Optional<Collection<Record>> array =
177                                    record.getOptionalArray(Record.class, entry.getName());
178                            final RecordVisitor<T> recordArrayVisitor = visitor.onRecordArray(entry, array);
179                            if (recordArrayVisitor != null) {
180                                array.ifPresent(a -> a.forEach(r -> {
181                                    final T visited = visit(recordArrayVisitor, r);
182                                    if (visited != null) {
183                                        final T current = out.get();
184                                        out.set(current == null ? visited : visitor.apply(current, visited));
185                                    }
186                                }));
187                            }
188                            break;
189                        // array of array is not yet supported!
190                        default:
191                            throw new IllegalStateException("Unsupported entry type: " + entry);
192                    }
193                    break;
194                default:
195                    throw new IllegalStateException("Unsupported entry type: " + entry);
196            }
197        });
198        final T value = out.get();
199        final T visited = visitor.get();
200        if (value != null) {
201            return visitor.apply(value, visited);
202        }
203        return visited;
204    }
205
206    @Override
207    public <T> T toObject(final Record data, final Class<T> expected) {
208        return expected
209                .cast(recordConverters
210                        .toType(mappingRegistry, data, expected, jsonBuilderFactorySupplier, jsonProvider,
211                                jsonbSupplier, () -> recordBuilderFactory));
212    }
213
214    @Override
215    public <T> Record toRecord(final T data) {
216        return recordConverters.toRecord(mappingRegistry, data, jsonbSupplier, () -> recordBuilderFactory);
217    }
218
219    @Override
220    public boolean forwardEntry(final Record source, final Record.Builder builder, final String sourceColumn,
221            final Schema.Entry entry) {
222        switch (entry.getType()) {
223            case INT:
224                final OptionalInt optionalInt = source.getOptionalInt(sourceColumn);
225                optionalInt.ifPresent(v -> builder.withInt(entry, v));
226                return optionalInt.isPresent();
227            case LONG:
228                final OptionalLong optionalLong = source.getOptionalLong(sourceColumn);
229                optionalLong.ifPresent(v -> builder.withLong(entry, v));
230                return optionalLong.isPresent();
231            case FLOAT:
232                final OptionalDouble optionalFloat = source.getOptionalFloat(sourceColumn);
233                optionalFloat.ifPresent(v -> builder.withFloat(entry, (float) v));
234                return optionalFloat.isPresent();
235            case DOUBLE:
236                final OptionalDouble optionalDouble = source.getOptionalDouble(sourceColumn);
237                optionalDouble.ifPresent(v -> builder.withDouble(entry, v));
238                return optionalDouble.isPresent();
239            case BOOLEAN:
240                final Optional<Boolean> optionalBoolean = source.getOptionalBoolean(sourceColumn);
241                optionalBoolean.ifPresent(v -> builder.withBoolean(entry, v));
242                return optionalBoolean.isPresent();
243            case STRING:
244                final Optional<String> optionalString = source.getOptionalString(sourceColumn);
245                optionalString.ifPresent(v -> builder.withString(entry, v));
246                return optionalString.isPresent();
247            case DATETIME:
248                final Optional<ZonedDateTime> optionalDateTime = source.getOptionalDateTime(sourceColumn);
249                optionalDateTime.ifPresent(v -> builder.withDateTime(entry, v));
250                return optionalDateTime.isPresent();
251            case DECIMAL:
252                final Optional<BigDecimal> optionalDecimal = source.getOptionalDecimal(sourceColumn);
253                optionalDecimal.ifPresent(v -> builder.withDecimal(entry, v));
254                return optionalDecimal.isPresent();
255            case BYTES:
256                final Optional<byte[]> optionalBytes = source.getOptionalBytes(sourceColumn);
257                optionalBytes.ifPresent(v -> builder.withBytes(entry, v));
258                return optionalBytes.isPresent();
259            case RECORD:
260                final Optional<Record> optionalRecord = source.getOptionalRecord(sourceColumn);
261                optionalRecord.ifPresent(v -> builder.withRecord(entry, v));
262                return optionalRecord.isPresent();
263            case ARRAY:
264                final Optional<Collection<Object>> optionalArray = source.getOptionalArray(Object.class, sourceColumn);
265                optionalArray.ifPresent(v -> builder.withArray(entry, v));
266                return optionalArray.isPresent();
267            default:
268                throw new IllegalStateException("Unsupported entry type: " + entry);
269        }
270    }
271
272    Object writeReplace() {
273        return new SerializableService(plugin, RecordService.class.getName());
274    }
275}