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.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, record.getOptionalArray(ZonedDateTime.class, entry.getName()));
167                    break;
168                case DECIMAL:
169                    visitor.onDecimalArray(entry, record.getOptionalArray(BigDecimal.class, entry.getName()));
170                    break;
171                case BYTES:
172                    visitor.onBytesArray(entry, record.getOptionalArray(byte[].class, entry.getName()));
173                    break;
174                case RECORD:
175                    final Optional<Collection<Record>> array = record.getOptionalArray(Record.class, entry.getName());
176                    final RecordVisitor<T> recordArrayVisitor = visitor.onRecordArray(entry, array);
177                    array.ifPresent(a -> a.forEach(r -> {
178                        final T visited = visit(recordArrayVisitor, r);
179                        if (visited != null) {
180                            final T current = out.get();
181                            out.set(current == null ? visited : visitor.apply(current, visited));
182                        }
183                    }));
184                    break;
185                // array of array is not yet supported!
186                default:
187                    throw new IllegalStateException("Unsupported entry type: " + entry);
188                }
189                break;
190            default:
191                throw new IllegalStateException("Unsupported entry type: " + entry);
192            }
193        });
194        final T value = out.get();
195        final T visited = visitor.get();
196        if (value != null) {
197            return visitor.apply(value, visited);
198        }
199        return visited;
200    }
201
202    @Override
203    public <T> T toObject(final Record data, final Class<T> expected) {
204        return expected
205                .cast(recordConverters
206                        .toType(mappingRegistry, data, expected, jsonBuilderFactorySupplier, jsonProvider,
207                                jsonbSupplier, () -> recordBuilderFactory));
208    }
209
210    @Override
211    public <T> Record toRecord(final T data) {
212        return recordConverters.toRecord(mappingRegistry, data, jsonbSupplier, () -> recordBuilderFactory);
213    }
214
215    @Override
216    public boolean forwardEntry(final Record source, final Record.Builder builder, final String sourceColumn,
217            final Schema.Entry entry) {
218        switch (entry.getType()) {
219        case INT:
220            final OptionalInt optionalInt = source.getOptionalInt(sourceColumn);
221            optionalInt.ifPresent(v -> builder.withInt(entry, v));
222            return optionalInt.isPresent();
223        case LONG:
224            final OptionalLong optionalLong = source.getOptionalLong(sourceColumn);
225            optionalLong.ifPresent(v -> builder.withLong(entry, v));
226            return optionalLong.isPresent();
227        case FLOAT:
228            final OptionalDouble optionalFloat = source.getOptionalFloat(sourceColumn);
229            optionalFloat.ifPresent(v -> builder.withFloat(entry, (float) v));
230            return optionalFloat.isPresent();
231        case DOUBLE:
232            final OptionalDouble optionalDouble = source.getOptionalDouble(sourceColumn);
233            optionalDouble.ifPresent(v -> builder.withDouble(entry, v));
234            return optionalDouble.isPresent();
235        case BOOLEAN:
236            final Optional<Boolean> optionalBoolean = source.getOptionalBoolean(sourceColumn);
237            optionalBoolean.ifPresent(v -> builder.withBoolean(entry, v));
238            return optionalBoolean.isPresent();
239        case STRING:
240            final Optional<String> optionalString = source.getOptionalString(sourceColumn);
241            optionalString.ifPresent(v -> builder.withString(entry, v));
242            return optionalString.isPresent();
243        case DATETIME:
244            final Optional<ZonedDateTime> optionalDateTime = source.getOptionalDateTime(sourceColumn);
245            optionalDateTime.ifPresent(v -> builder.withDateTime(entry, v));
246            return optionalDateTime.isPresent();
247        case DECIMAL:
248            final Optional<BigDecimal> optionalDecimal = source.getOptionalDecimal(sourceColumn);
249            optionalDecimal.ifPresent(v -> builder.withDecimal(entry, v));
250            return optionalDecimal.isPresent();
251        case BYTES:
252            final Optional<byte[]> optionalBytes = source.getOptionalBytes(sourceColumn);
253            optionalBytes.ifPresent(v -> builder.withBytes(entry, v));
254            return optionalBytes.isPresent();
255        case RECORD:
256            final Optional<Record> optionalRecord = source.getOptionalRecord(sourceColumn);
257            optionalRecord.ifPresent(v -> builder.withRecord(entry, v));
258            return optionalRecord.isPresent();
259        case ARRAY:
260            final Optional<Collection<Object>> optionalArray = source.getOptionalArray(Object.class, sourceColumn);
261            optionalArray.ifPresent(v -> builder.withArray(entry, v));
262            return optionalArray.isPresent();
263        default:
264            throw new IllegalStateException("Unsupported entry type: " + entry);
265        }
266    }
267
268    Object writeReplace() {
269        return new SerializableService(plugin, RecordService.class.getName());
270    }
271}