001/**
002 * Copyright (C) 2006-2022 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}