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.visitor;
017
018import static java.util.stream.Collectors.toList;
019
020import java.lang.annotation.Annotation;
021import java.lang.reflect.Method;
022import java.lang.reflect.Parameter;
023import java.lang.reflect.ParameterizedType;
024import java.lang.reflect.Type;
025import java.math.BigDecimal;
026import java.util.Arrays;
027import java.util.Collection;
028import java.util.Date;
029import java.util.HashSet;
030import java.util.List;
031import java.util.Map;
032import java.util.Optional;
033import java.util.Set;
034import java.util.stream.Collectors;
035import java.util.stream.Stream;
036
037import org.talend.sdk.component.api.component.AfterVariables.AfterVariable;
038import org.talend.sdk.component.api.component.AfterVariables.AfterVariableContainer;
039import org.talend.sdk.component.api.input.Assessor;
040import org.talend.sdk.component.api.input.Emitter;
041import org.talend.sdk.component.api.input.PartitionMapper;
042import org.talend.sdk.component.api.input.PartitionSize;
043import org.talend.sdk.component.api.input.Producer;
044import org.talend.sdk.component.api.input.Split;
045import org.talend.sdk.component.api.processor.AfterGroup;
046import org.talend.sdk.component.api.processor.BeforeGroup;
047import org.talend.sdk.component.api.processor.ElementListener;
048import org.talend.sdk.component.api.processor.LastGroup;
049import org.talend.sdk.component.api.processor.Output;
050import org.talend.sdk.component.api.processor.OutputEmitter;
051import org.talend.sdk.component.api.processor.Processor;
052import org.talend.sdk.component.api.standalone.DriverRunner;
053import org.talend.sdk.component.api.standalone.RunAtDriver;
054import org.talend.sdk.component.runtime.reflect.Parameters;
055
056public class ModelVisitor {
057
058    private static final Set<Class<?>> SUPPORTED_AFTER_VARIABLES_TYPES = new HashSet<>(Arrays
059            .asList(Boolean.class, Byte.class, byte[].class, Character.class, Date.class, Double.class, Float.class,
060                    BigDecimal.class, Integer.class, Long.class, Object.class, Short.class, String.class, List.class));
061
062    public static final String MUST_NOT_HAVE_ANY_PARAMETER = " must not have any parameter";
063
064    public void visit(final Class<?> type, final ModelListener listener, final boolean validate) {
065        if (getSupportedComponentTypes().noneMatch(type::isAnnotationPresent)) { // unlikely but just in case
066            return;
067        }
068        if (getSupportedComponentTypes().filter(type::isAnnotationPresent).count() != 1) { // > 1 actually
069            throw new IllegalArgumentException("You can't mix @Emitter, @PartitionMapper and @Processor on " + type);
070        }
071
072        if (type.isAnnotationPresent(PartitionMapper.class)) {
073            if (validate) {
074                validatePartitionMapper(type);
075            }
076            listener.onPartitionMapper(type, type.getAnnotation(PartitionMapper.class));
077        } else if (type.isAnnotationPresent(Emitter.class)) {
078            if (validate) {
079                validateEmitter(type);
080            }
081            listener.onEmitter(type, type.getAnnotation(Emitter.class));
082        } else if (type.isAnnotationPresent(Processor.class)) {
083            if (validate) {
084                validateProcessor(type);
085            }
086            listener.onProcessor(type, type.getAnnotation(Processor.class));
087        } else if (type.isAnnotationPresent(DriverRunner.class)) {
088            if (validate) {
089                validateDriverRunner(type);
090            }
091            listener.onDriverRunner(type, type.getAnnotation(DriverRunner.class));
092        }
093    }
094
095    private void validatePartitionMapper(final Class<?> type) {
096        final boolean infinite = type.getAnnotation(PartitionMapper.class).infinite();
097        final long count = Stream
098                .of(type.getMethods())
099                .filter(m -> getPartitionMapperMethods(infinite).anyMatch(m::isAnnotationPresent))
100                .flatMap(m -> getPartitionMapperMethods(infinite).filter(m::isAnnotationPresent))
101                .distinct()
102                .count();
103        if (count != (infinite ? 2 : 3)) {
104            throw new IllegalArgumentException(
105                    type + " partition mapper must have exactly one @Assessor (if not infinite), "
106                            + "one @Split and one @Emitter methods");
107        }
108        final boolean stoppable = type.getAnnotation(PartitionMapper.class).stoppable();
109        if (!infinite && stoppable) {
110            throw new IllegalArgumentException(type + " partition mapper when not infinite cannot set stoppable");
111        }
112        //
113        // now validate the 2 methods of the mapper
114        //
115        if (!infinite) {
116            Stream.of(type.getMethods()).filter(m -> m.isAnnotationPresent(Assessor.class)).forEach(m -> {
117                if (m.getParameterCount() > 0) {
118                    throw new IllegalArgumentException(m + MUST_NOT_HAVE_ANY_PARAMETER);
119                }
120            });
121        }
122        Stream.of(type.getMethods()).filter(m -> m.isAnnotationPresent(Split.class)).forEach(m -> {
123            // for now, we could inject it by default but to ensure we can inject more later
124            // we must do that validation
125            if (Stream
126                    .of(m.getParameters())
127                    .anyMatch(p -> !p.isAnnotationPresent(PartitionSize.class)
128                            || (p.getType() != long.class && p.getType() != int.class))) {
129                throw new IllegalArgumentException(m + " must not have any parameter without @PartitionSize");
130            }
131            final Type splitReturnType = m.getGenericReturnType();
132            if (!ParameterizedType.class.isInstance(splitReturnType)) {
133                throw new IllegalArgumentException(m + " must return a Collection<" + type.getName() + ">");
134            }
135
136            final ParameterizedType splitPt = ParameterizedType.class.cast(splitReturnType);
137            if (!Class.class.isInstance(splitPt.getRawType())
138                    || !Collection.class.isAssignableFrom(Class.class.cast(splitPt.getRawType()))) {
139                throw new IllegalArgumentException(m + " must return a List of partition mapper, found: " + splitPt);
140            }
141
142            final Type arg = splitPt.getActualTypeArguments().length != 1 ? null : splitPt.getActualTypeArguments()[0];
143            if (!Class.class.isInstance(arg) || !type.isAssignableFrom(Class.class.cast(arg))) {
144                throw new IllegalArgumentException(
145                        m + " must return a Collection<" + type.getName() + "> but found: " + arg);
146            }
147        });
148        Stream
149                .of(type.getMethods())
150                .filter(m -> m.isAnnotationPresent(Emitter.class))
151                .forEach(m -> {
152                    // for now we don't support injection propagation since the mapper should
153                    // already own all the config
154                    if (m.getParameterCount() > 0) {
155                        throw new IllegalArgumentException(m + MUST_NOT_HAVE_ANY_PARAMETER);
156                    }
157                });
158
159        validateAfterVariableAnnotationDeclaration(type);
160        validateAfterVariableContainer(type);
161    }
162
163    private void validateEmitter(final Class<?> input) {
164        final List<Method> producers =
165                Stream.of(input.getMethods()).filter(m -> m.isAnnotationPresent(Producer.class)).collect(toList());
166        if (producers.size() != 1) {
167            throw new IllegalArgumentException(input + " must have a single @Producer method");
168        }
169
170        if (producers.get(0).getParameterCount() > 0) {
171            throw new IllegalArgumentException(producers.get(0) + MUST_NOT_HAVE_ANY_PARAMETER);
172        }
173
174        validateAfterVariableAnnotationDeclaration(input);
175        validateAfterVariableContainer(input);
176    }
177
178    private void validateDriverRunner(final Class<?> standalone) {
179        final List<Method> driverRunners = Stream
180                .of(standalone.getMethods())
181                .filter(m -> m.isAnnotationPresent(RunAtDriver.class))
182                .collect(toList());
183        if (driverRunners.size() != 1) {
184            throw new IllegalArgumentException(standalone + " must have a single @RunAtDriver method");
185        }
186
187        if (driverRunners.get(0).getParameterCount() > 0) {
188            throw new IllegalArgumentException(driverRunners.get(0) + MUST_NOT_HAVE_ANY_PARAMETER);
189        }
190
191        validateAfterVariableAnnotationDeclaration(standalone);
192        validateAfterVariableContainer(standalone);
193    }
194
195    private void validateProcessor(final Class<?> input) {
196        final List<Method> afterGroups =
197                Stream.of(input.getMethods()).filter(m -> m.isAnnotationPresent(AfterGroup.class)).collect(toList());
198        afterGroups.forEach(m -> {
199            final List<Parameter> invalidParams = Stream.of(m.getParameters()).peek(p -> {
200                if (p.isAnnotationPresent(Output.class) && !validOutputParam(p)) {
201                    throw new IllegalArgumentException("@Output parameter must be of type OutputEmitter");
202                }
203            })
204                    .filter(p -> !p.isAnnotationPresent(Output.class))
205                    .filter(p -> !p.isAnnotationPresent(LastGroup.class))
206                    .filter(p -> !Parameters.isGroupBuffer(p.getParameterizedType()))
207                    .collect(toList());
208            if (!invalidParams.isEmpty()) {
209                throw new IllegalArgumentException("Parameter of AfterGroup method need to be annotated with Output");
210            }
211        });
212        if (afterGroups
213                .stream()
214                .anyMatch(m -> Stream.of(m.getParameters()).anyMatch(p -> p.isAnnotationPresent(LastGroup.class)))
215                && afterGroups.size() > 1) {
216            throw new IllegalArgumentException(input
217                    + " must have a single @AfterGroup method with @LastGroup parameter");
218        }
219
220        validateProducer(input, afterGroups);
221
222        Stream.of(input.getMethods()).filter(m -> m.isAnnotationPresent(BeforeGroup.class)).forEach(m -> {
223            if (m.getParameterCount() > 0) {
224                throw new IllegalArgumentException(m + MUST_NOT_HAVE_ANY_PARAMETER);
225            }
226        });
227
228        validateAfterVariableAnnotationDeclaration(input);
229        validateAfterVariableContainer(input);
230    }
231
232    private void validateProducer(final Class<?> input, final List<Method> afterGroups) {
233        final List<Method> producers = Stream
234                .of(input.getMethods())
235                .filter(m -> m.isAnnotationPresent(ElementListener.class))
236                .collect(toList());
237        if (producers.size() > 1) {
238            throw new IllegalArgumentException(input + " must have a single @ElementListener method");
239        }
240        if (producers.isEmpty() && afterGroups
241                .stream()
242                .noneMatch(m -> Stream.of(m.getGenericParameterTypes()).anyMatch(Parameters::isGroupBuffer))) {
243            throw new IllegalArgumentException(input
244                    + " must have a single @ElementListener method or pass records as a Collection<Record|JsonObject> to its @AfterGroup method");
245        }
246
247        if (!producers.isEmpty() && Stream.of(producers.get(0).getParameters()).peek(p -> {
248            if (p.isAnnotationPresent(Output.class) && !validOutputParam(p)) {
249                throw new IllegalArgumentException("@Output parameter must be of type OutputEmitter");
250            }
251        }).filter(p -> !p.isAnnotationPresent(Output.class)).count() < 1) {
252            throw new IllegalArgumentException(input + " doesn't have the input parameter on its producer method");
253        }
254    }
255
256    private boolean validOutputParam(final Parameter p) {
257        if (!ParameterizedType.class.isInstance(p.getParameterizedType())) {
258            return false;
259        }
260        final ParameterizedType pt = ParameterizedType.class.cast(p.getParameterizedType());
261        return OutputEmitter.class == pt.getRawType();
262    }
263
264    private Stream<Class<? extends Annotation>> getPartitionMapperMethods(final boolean infinite) {
265        return infinite ? Stream.of(Split.class, Emitter.class) : Stream.of(Assessor.class, Split.class, Emitter.class);
266    }
267
268    private Stream<Class<? extends Annotation>> getSupportedComponentTypes() {
269        return Stream.of(Emitter.class, PartitionMapper.class, Processor.class, DriverRunner.class);
270    }
271
272    private void validateAfterVariableContainer(final Class<?> type) {
273        // component can't have more than one after variable container
274        List<Method> markedMethods = Stream
275                .of(type.getMethods())
276                .filter(m -> m.isAnnotationPresent(AfterVariableContainer.class))
277                .collect(toList());
278        if (markedMethods.size() > 1) {
279            String methods = markedMethods.stream().map(Method::toGenericString).collect(Collectors.joining(","));
280            throw new IllegalArgumentException("The methods can't have more than 1 after variable container. "
281                    + "Current marked methods: " + methods);
282        }
283
284        // check parameter list
285        Optional
286                .of(markedMethods
287                        .stream()
288                        .filter(m -> m.getParameterCount() != 0)
289                        .map(Method::toGenericString)
290                        .collect(Collectors.joining(",")))
291                .filter(str -> !str.isEmpty())
292                .ifPresent(str -> {
293                    throw new IllegalArgumentException(
294                            "The method is annotated with " + AfterVariableContainer.class.getCanonicalName() + "'"
295                                    + str + "' should have parameters.");
296                });
297
298        // check incorrect return type
299        Optional
300                .of(markedMethods
301                        .stream()
302                        .filter(m -> !isValidAfterVariableContainer(m.getGenericReturnType()))
303                        .map(Method::toGenericString)
304                        .collect(Collectors.joining(",")))
305                .filter(it -> !it.isEmpty())
306                .ifPresent(methods -> {
307                    throw new IllegalArgumentException(
308                            "The method '" + methods + "' has wrong return type. It should be Map<String, Object>.");
309                });
310    }
311
312    /**
313     * Right now the valid container object for after variables is Map.
314     * Where the key is String and value is Object
315     */
316    private static boolean isValidAfterVariableContainer(final Type type) {
317        if (!(type instanceof ParameterizedType)) {
318            return false;
319        }
320
321        final ParameterizedType paramType = (ParameterizedType) type;
322        if (!(paramType.getRawType() instanceof Class) || paramType.getActualTypeArguments().length != 2) {
323            return false;
324        }
325
326        final Class<?> containerType = (Class<?>) paramType.getRawType();
327        return Map.class.isAssignableFrom(containerType) && paramType.getActualTypeArguments()[0].equals(String.class)
328                && paramType.getActualTypeArguments()[1].equals(Object.class);
329    }
330
331    private static void validateAfterVariableAnnotationDeclaration(final Class<?> type) {
332        List<String> incorrectDeclarations = Stream
333                .of(type.getAnnotationsByType(AfterVariable.class))
334                .filter(annotation -> !SUPPORTED_AFTER_VARIABLES_TYPES.contains(annotation.type()))
335                .map(annotation -> "The after variable with name '" + annotation.value() + "' has incorrect type: '"
336                        + annotation.type() + "'")
337                .collect(toList());
338        if (!incorrectDeclarations.isEmpty()) {
339            String message = incorrectDeclarations
340                    .stream()
341                    .collect(Collectors.joining(",", "The after variables declared incorrectly. ", ""));
342            throw new IllegalArgumentException(message);
343        }
344    }
345}