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.output; 017 018import static java.util.Collections.emptyList; 019import static java.util.Collections.emptyMap; 020import static java.util.Optional.ofNullable; 021import static java.util.stream.Collectors.toList; 022import static java.util.stream.Collectors.toMap; 023import static org.talend.sdk.component.runtime.reflect.Parameters.isGroupBuffer; 024 025import java.io.ByteArrayInputStream; 026import java.io.IOException; 027import java.io.InvalidObjectException; 028import java.io.ObjectInputStream; 029import java.io.ObjectStreamException; 030import java.io.Serializable; 031import java.lang.reflect.Method; 032import java.lang.reflect.Parameter; 033import java.lang.reflect.ParameterizedType; 034import java.util.AbstractMap; 035import java.util.ArrayList; 036import java.util.Collection; 037import java.util.Collections; 038import java.util.List; 039import java.util.Map; 040import java.util.function.BiFunction; 041import java.util.function.Function; 042import java.util.stream.Stream; 043 044import javax.json.Json; 045import javax.json.JsonBuilderFactory; 046import javax.json.bind.Jsonb; 047import javax.json.bind.JsonbBuilder; 048import javax.json.bind.JsonbConfig; 049import javax.json.bind.config.BinaryDataStrategy; 050import javax.json.spi.JsonProvider; 051 052import org.talend.sdk.component.api.processor.AfterGroup; 053import org.talend.sdk.component.api.processor.BeforeGroup; 054import org.talend.sdk.component.api.processor.ElementListener; 055import org.talend.sdk.component.api.processor.Input; 056import org.talend.sdk.component.api.processor.Output; 057import org.talend.sdk.component.api.service.record.RecordBuilderFactory; 058import org.talend.sdk.component.runtime.base.Delegated; 059import org.talend.sdk.component.runtime.base.LifecycleImpl; 060import org.talend.sdk.component.runtime.record.RecordBuilderFactoryImpl; 061import org.talend.sdk.component.runtime.record.RecordConverters; 062import org.talend.sdk.component.runtime.serialization.ContainerFinder; 063import org.talend.sdk.component.runtime.serialization.EnhancedObjectInputStream; 064 065import lombok.AllArgsConstructor; 066 067public class ProcessorImpl extends LifecycleImpl implements Processor, Delegated { 068 069 private transient List<Method> beforeGroup; 070 071 private transient List<Method> afterGroup; 072 073 private transient Method process; 074 075 private transient List<BiFunction<InputFactory, OutputFactory, Object>> parameterBuilderProcess; 076 077 private transient Map<Method, List<Function<OutputFactory, Object>>> parameterBuilderAfterGroup; 078 079 private transient Jsonb jsonb; 080 081 private transient JsonBuilderFactory jsonBuilderFactory; 082 083 private transient RecordBuilderFactory recordBuilderFactory; 084 085 private transient JsonProvider jsonProvider; 086 087 private transient boolean forwardReturn; 088 089 private transient RecordConverters converter; 090 091 private transient Class<?> expectedRecordType; 092 093 private transient Collection<Object> records; 094 095 private Map<String, String> internalConfiguration; 096 097 private RecordConverters.MappingMetaRegistry mappings; 098 099 public ProcessorImpl(final String rootName, final String name, final String plugin, 100 final Map<String, String> internalConfiguration, final Serializable delegate) { 101 super(delegate, rootName, name, plugin); 102 this.internalConfiguration = internalConfiguration; 103 } 104 105 protected ProcessorImpl() { 106 // no-op 107 } 108 109 public Map<String, String> getInternalConfiguration() { 110 return ofNullable(internalConfiguration).orElseGet(Collections::emptyMap); 111 } 112 113 @Override 114 public void beforeGroup() { 115 if (beforeGroup == null) { 116 beforeGroup = findMethods(BeforeGroup.class).collect(toList()); 117 afterGroup = findMethods(AfterGroup.class).collect(toList()); 118 process = findMethods(ElementListener.class).findFirst().orElse(null); 119 120 // IMPORTANT: ensure you call only once the create(....), see studio integration (mojo) 121 parameterBuilderProcess = process == null ? emptyList() 122 : Stream.of(process.getParameters()).map(this::buildProcessParamBuilder).collect(toList()); 123 parameterBuilderAfterGroup = afterGroup 124 .stream() 125 .map(after -> new AbstractMap.SimpleEntry<>(after, Stream.of(after.getParameters()).map(param -> { 126 if (isGroupBuffer(param.getParameterizedType())) { 127 expectedRecordType = Class.class 128 .cast(ParameterizedType.class 129 .cast(param.getParameterizedType()) 130 .getActualTypeArguments()[0]); 131 return (Function<OutputFactory, Object>) o -> records; 132 } 133 return toOutputParamBuilder(param); 134 }).collect(toList()))) 135 .collect(toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue)); 136 forwardReturn = process != null && process.getReturnType() != void.class; 137 138 converter = new RecordConverters(); 139 140 mappings = new RecordConverters.MappingMetaRegistry(); 141 } 142 143 beforeGroup.forEach(this::doInvoke); 144 if (process == null) { // collect records for @AfterGroup param 145 records = new ArrayList<>(); 146 } 147 } 148 149 private BiFunction<InputFactory, OutputFactory, Object> buildProcessParamBuilder(final Parameter parameter) { 150 if (parameter.isAnnotationPresent(Output.class)) { 151 return (inputs, outputs) -> { 152 final String name = parameter.getAnnotation(Output.class).value(); 153 return outputs.create(name); 154 }; 155 } 156 157 final Class<?> parameterType = parameter.getType(); 158 final String inputName = 159 ofNullable(parameter.getAnnotation(Input.class)).map(Input::value).orElse(Branches.DEFAULT_BRANCH); 160 return (inputs, outputs) -> doConvertInput(parameterType, inputs.read(inputName)); 161 } 162 163 private Function<OutputFactory, Object> toOutputParamBuilder(final Parameter parameter) { 164 return outputs -> { 165 final String name = parameter.getAnnotation(Output.class).value(); 166 return outputs.create(name); 167 }; 168 } 169 170 private Object doConvertInput(final Class<?> parameterType, final Object data) { 171 if (data == null || parameterType.isInstance(data) 172 || parameterType.isPrimitive() /* mainly for tests, no > manager */) { 173 return data; 174 } 175 return converter 176 .toType(mappings, data, parameterType, this::jsonBuilderFactory, this::jsonProvider, this::jsonb, 177 this::recordBuilderFactory); 178 } 179 180 private Jsonb jsonb() { 181 if (jsonb != null) { 182 return jsonb; 183 } 184 synchronized (this) { 185 if (jsonb == null) { 186 jsonb = ContainerFinder.Instance.get().find(plugin()).findService(Jsonb.class); 187 } 188 if (jsonb == null) { // for tests mainly 189 jsonb = JsonbBuilder.create(new JsonbConfig().withBinaryDataStrategy(BinaryDataStrategy.BASE_64)); 190 } 191 } 192 return jsonb; 193 } 194 195 private RecordBuilderFactory recordBuilderFactory() { 196 if (recordBuilderFactory != null) { 197 return recordBuilderFactory; 198 } 199 synchronized (this) { 200 if (recordBuilderFactory == null) { 201 recordBuilderFactory = 202 ContainerFinder.Instance.get().find(plugin()).findService(RecordBuilderFactory.class); 203 } 204 if (recordBuilderFactory == null) { 205 recordBuilderFactory = new RecordBuilderFactoryImpl("$volatile"); 206 } 207 } 208 209 return recordBuilderFactory; 210 } 211 212 private JsonBuilderFactory jsonBuilderFactory() { 213 if (jsonBuilderFactory != null) { 214 return jsonBuilderFactory; 215 } 216 synchronized (this) { 217 if (jsonBuilderFactory == null) { 218 jsonBuilderFactory = 219 ContainerFinder.Instance.get().find(plugin()).findService(JsonBuilderFactory.class); 220 } 221 if (jsonBuilderFactory == null) { 222 jsonBuilderFactory = Json.createBuilderFactory(emptyMap()); 223 } 224 } 225 return jsonBuilderFactory; 226 } 227 228 private JsonProvider jsonProvider() { 229 if (jsonProvider != null) { 230 return jsonProvider; 231 } 232 synchronized (this) { 233 if (jsonProvider == null) { 234 jsonProvider = ContainerFinder.Instance.get().find(plugin()).findService(JsonProvider.class); 235 } 236 } 237 return jsonProvider; 238 } 239 240 @Override 241 public void afterGroup(final OutputFactory output) { 242 afterGroup 243 .forEach(after -> doInvoke(after, 244 parameterBuilderAfterGroup 245 .get(after) 246 .stream() 247 .map(b -> b.apply(output)) 248 .toArray(Object[]::new))); 249 if (records != null) { 250 records = null; 251 } 252 } 253 254 @Override 255 public void onNext(final InputFactory inputFactory, final OutputFactory outputFactory) { 256 if (process == null) { 257 // todo: handle @Input there too? less likely it becomes useful 258 records.add(doConvertInput(expectedRecordType, inputFactory.read(Branches.DEFAULT_BRANCH))); 259 } else { 260 final Object[] args = parameterBuilderProcess 261 .stream() 262 .map(b -> b.apply(inputFactory, outputFactory)) 263 .toArray(Object[]::new); 264 final Object out = doInvoke(process, args); 265 if (forwardReturn) { 266 outputFactory.create(Branches.DEFAULT_BRANCH).emit(out); 267 } 268 } 269 } 270 271 @Override 272 public Object getDelegate() { 273 return delegate; 274 } 275 276 Object writeReplace() throws ObjectStreamException { 277 return new SerializationReplacer(plugin(), rootName(), name(), internalConfiguration, serializeDelegate()); 278 } 279 280 protected static Serializable loadDelegate(final byte[] value, final String plugin) 281 throws IOException, ClassNotFoundException { 282 try (final ObjectInputStream ois = new EnhancedObjectInputStream(new ByteArrayInputStream(value), 283 ContainerFinder.Instance.get().find(plugin).classloader())) { 284 return Serializable.class.cast(ois.readObject()); 285 } 286 } 287 288 @AllArgsConstructor 289 private static class SerializationReplacer implements Serializable { 290 291 private final String plugin; 292 293 private final String component; 294 295 private final String name; 296 297 private final Map<String, String> internalConfiguration; 298 299 private final byte[] value; 300 301 Object readResolve() throws ObjectStreamException { 302 try { 303 return new ProcessorImpl(component, name, plugin, internalConfiguration, loadDelegate(value, plugin)); 304 } catch (final IOException | ClassNotFoundException e) { 305 final InvalidObjectException invalidObjectException = new InvalidObjectException(e.getMessage()); 306 invalidObjectException.initCause(e); 307 throw invalidObjectException; 308 } 309 } 310 } 311}