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