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 java.util.stream.Collectors.toMap;
019import static java.util.stream.Stream.concat;
020import static java.util.stream.Stream.empty;
021import static java.util.stream.Stream.of;
022import static org.talend.sdk.component.runtime.manager.ParameterMeta.Type.ARRAY;
023import static org.talend.sdk.component.runtime.manager.ParameterMeta.Type.ENUM;
024import static org.talend.sdk.component.runtime.manager.ParameterMeta.Type.OBJECT;
025
026import java.lang.reflect.ParameterizedType;
027import java.lang.reflect.Type;
028import java.util.AbstractMap;
029import java.util.List;
030import java.util.Map;
031import java.util.Objects;
032import java.util.Set;
033import java.util.stream.Collectors;
034import java.util.stream.Stream;
035
036import org.apache.xbean.finder.AnnotationFinder;
037import org.talend.sdk.component.runtime.manager.ParameterMeta;
038import org.talend.sdk.component.tools.validator.Validators.ValidatorHelper;
039
040import lombok.extern.slf4j.Slf4j;
041
042@Slf4j
043public class LayoutValidator implements Validator {
044
045    private final Validators.ValidatorHelper helper;
046
047    public LayoutValidator(final ValidatorHelper helper) {
048        this.helper = helper;
049    }
050
051    @Override
052    public Stream<String> validate(final AnnotationFinder finder, final List<Class<?>> components) {
053        return components
054                .stream()
055                .map(helper::buildOrGetParameters)
056                .flatMap(this::toFlatNonPrimitiveConfig)
057                .collect(toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue, (p1, p2) -> p1))
058                .entrySet()
059                .stream()
060                .flatMap(this::visitLayout);
061    }
062
063    private Stream<String> visitLayout(final Map.Entry<String, ParameterMeta> config) {
064
065        final Set<String> fieldsInGridLayout = config
066                .getValue()
067                .getMetadata()
068                .entrySet()
069                .stream()
070                .filter(meta -> meta.getKey().startsWith("tcomp::ui::gridlayout"))
071                .flatMap(meta -> of(meta.getValue().split("\\|")))
072                .flatMap(s -> of(s.split(",")))
073                .filter(s -> !s.isEmpty())
074                .collect(Collectors.toSet());
075
076        final Set<String> fieldsInOptionOrder = config
077                .getValue()
078                .getMetadata()
079                .entrySet()
080                .stream()
081                .filter(meta -> meta.getKey().startsWith("tcomp::ui::optionsorder"))
082                .flatMap(meta -> of(meta.getValue().split(",")))
083                .collect(Collectors.toSet());
084
085        if (fieldsInGridLayout.isEmpty() && fieldsInOptionOrder.isEmpty()) {
086            return Stream.empty();
087        }
088
089        if (!fieldsInGridLayout.isEmpty() && !fieldsInOptionOrder.isEmpty()) {
090            this.log.error("Concurrent layout found for '" + config.getKey() + "', the @OptionsOrder will be ignored.");
091        }
092
093        final Stream<String> errorsLib;
094        if (!fieldsInGridLayout.isEmpty()) {
095            errorsLib = fieldsInGridLayout
096                    .stream()
097                    .filter(fieldInLayout -> config
098                            .getValue()
099                            .getNestedParameters()
100                            .stream()
101                            .map(ParameterMeta::getName)
102                            .noneMatch(field -> field.equals(fieldInLayout)))
103                    .map(fieldInLayout -> "Option '" + fieldInLayout
104                            + "' in @GridLayout doesn't exist in declaring class '" + config.getKey() + "'")
105                    .sorted();
106
107            config
108                    .getValue()
109                    .getNestedParameters()
110                    .stream()
111                    .filter(field -> !fieldsInGridLayout.contains(field.getName()))
112                    .map(field -> "Field '" + field.getName() + "' in " + config.getKey()
113                            + " is not declared in any layout.")
114                    .forEach(this.log::error);
115        } else {
116            errorsLib = fieldsInOptionOrder
117                    .stream()
118                    .filter(fieldInLayout -> config
119                            .getValue()
120                            .getNestedParameters()
121                            .stream()
122                            .map(ParameterMeta::getName)
123                            .noneMatch(field -> field.equals(fieldInLayout)))
124                    .map(fieldInLayout -> "Option '" + fieldInLayout
125                            + "' in @OptionOrder doesn't exist in declaring class '" + config.getKey() + "'")
126                    .sorted();
127
128            config
129                    .getValue()
130                    .getNestedParameters()
131                    .stream()
132                    .filter(field -> !fieldsInOptionOrder.contains(field.getName()))
133                    .map(field -> "Field '" + field.getName() + "' in " + config.getKey()
134                            + " is not declared in any layout.")
135                    .forEach(this.log::error);
136        }
137        return errorsLib;
138    }
139
140    private Stream<AbstractMap.SimpleEntry<String, ParameterMeta>>
141            toFlatNonPrimitiveConfig(final List<ParameterMeta> config) {
142        if (config == null || config.isEmpty()) {
143            return empty();
144        }
145        return config
146                .stream()
147                .filter(Objects::nonNull)
148                .filter(p -> OBJECT.equals(p.getType()) || isArrayOfObject(p))
149                .filter(p -> p.getNestedParameters() != null)
150                .flatMap(p -> concat(of(new AbstractMap.SimpleEntry<>(toJavaType(p).getName(), p)),
151                        toFlatNonPrimitiveConfig(p.getNestedParameters())));
152    }
153
154    private boolean isArrayOfObject(final ParameterMeta param) {
155
156        return ARRAY.equals(param.getType()) && param.getNestedParameters() != null
157                && param
158                        .getNestedParameters()
159                        .stream()
160                        .anyMatch(p -> OBJECT.equals(p.getType()) || ENUM.equals(p.getType()) || isArrayOfObject(p));
161
162    }
163
164    private Class<?> toJavaType(final ParameterMeta p) {
165        if (p.getType().equals(OBJECT) || p.getType().equals(ENUM)) {
166            if (Class.class.isInstance(p.getJavaType())) {
167                return Class.class.cast(p.getJavaType());
168            }
169            throw new IllegalArgumentException("Unsupported type for parameter " + p.getPath() + " (from "
170                    + p.getSource().declaringClass() + "), ensure it is a Class<?>");
171        }
172
173        if (p.getType().equals(ARRAY) && ParameterizedType.class.isInstance(p.getJavaType())) {
174            final ParameterizedType parameterizedType = ParameterizedType.class.cast(p.getJavaType());
175            final Type[] arguments = parameterizedType.getActualTypeArguments();
176            if (arguments.length == 1 && Class.class.isInstance(arguments[0])) {
177                return Class.class.cast(arguments[0]);
178            }
179            throw new IllegalArgumentException("Unsupported type for parameter " + p.getPath() + " (from "
180                    + p.getSource().declaringClass() + "), " + "ensure it is a ParameterizedType with one argument");
181        }
182
183        throw new IllegalStateException("Parameter '" + p.getName() + "' is not an object.");
184    }
185}