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}