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}