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.tools.validator;
017
018import static org.talend.sdk.component.runtime.manager.reflect.Constructors.findConstructor;
019
020import java.lang.annotation.Annotation;
021import java.lang.reflect.Field;
022import java.lang.reflect.Parameter;
023import java.lang.reflect.ParameterizedType;
024import java.util.List;
025import java.util.Objects;
026import java.util.stream.Stream;
027
028import org.apache.xbean.finder.AnnotationFinder;
029import org.talend.sdk.component.api.configuration.ui.widget.Structure;
030import org.talend.sdk.component.api.input.Emitter;
031import org.talend.sdk.component.api.input.PartitionMapper;
032import org.talend.sdk.component.api.processor.Processor;
033import org.talend.sdk.component.api.standalone.DriverRunner;
034import org.talend.sdk.component.runtime.visitor.ModelListener;
035import org.talend.sdk.component.runtime.visitor.ModelVisitor;
036import org.talend.sdk.component.tools.validator.Validators.ValidatorHelper;
037
038public class ModelValidator implements Validator {
039
040    private final boolean validateComponent;
041
042    private final ValidatorHelper helper;
043
044    public ModelValidator(final boolean validateComponent, final ValidatorHelper helper) {
045        this.validateComponent = validateComponent;
046        this.helper = helper;
047    }
048
049    @Override
050    public Stream<String> validate(final AnnotationFinder finder, final List<Class<?>> components) {
051        final Stream<String> errorsAnnotations = components
052                .stream()
053                .filter(this::containsIncompatibleAnnotation)
054                .map(i -> i + " has conflicting component annotations, ensure it has a single one")
055                .sorted();
056
057        final Stream<String> errorsParamConstructors = components
058                .stream()
059                .filter(c -> countParameters(findConstructor(c).getParameters()) > 1)
060                .map(c -> "Component must use a single root option. '" + c.getName() + "'")
061                .sorted();
062
063        final ModelVisitor modelVisitor = new ModelVisitor();
064        final ModelListener noop = new ModelListener() {
065        };
066
067        final Stream<String> errorsConfig = components.stream().map(c -> {
068            try {
069                modelVisitor.visit(c, noop, this.validateComponent);
070                return null;
071            } catch (final RuntimeException re) {
072                return re.getMessage();
073            }
074        }).filter(Objects::nonNull).sorted();
075
076        // limited config types
077        final Stream<String> errorStructure = finder
078                .findAnnotatedFields(Structure.class)
079                .stream()
080                .filter(f -> !ParameterizedType.class.isInstance(f.getGenericType()) || !isListObject(f))
081                .map(f -> f.getDeclaringClass() + "#" + f.getName()
082                        + " uses @Structure but is not a List<String> nor a List<Object>")
083                .sorted();
084
085        return Stream
086                .of(errorsAnnotations, errorsParamConstructors, errorsConfig, errorStructure)
087                .reduce(Stream::concat)
088                .orElse(Stream.empty());
089    }
090
091    private boolean containsIncompatibleAnnotation(final Class<?> clazz) {
092        return Stream
093                .of(PartitionMapper.class, Processor.class, Emitter.class, DriverRunner.class)
094                .filter((Class<? extends Annotation> an) -> clazz.isAnnotationPresent(an))
095                .count() > 1;
096
097    }
098
099    private int countParameters(final Parameter[] params) {
100        return (int) Stream.of(params).filter((Parameter p) -> !this.helper.isService(p)).count();
101    }
102
103    private boolean isListObject(final Field f) {
104        final ParameterizedType pt = ParameterizedType.class.cast(f.getGenericType());
105        return List.class == pt.getRawType() && pt.getActualTypeArguments().length == 1;
106    }
107
108}