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}