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