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;
017
018import static java.util.Collections.emptyList;
019import static java.util.Optional.ofNullable;
020import static java.util.stream.Collectors.joining;
021import static java.util.stream.Collectors.toCollection;
022import static java.util.stream.Collectors.toList;
023import static java.util.stream.Stream.of;
024import static org.talend.sdk.component.runtime.manager.reflect.Constructors.findConstructor;
025
026import java.io.File;
027import java.lang.reflect.Parameter;
028import java.util.ArrayList;
029import java.util.Collection;
030import java.util.HashMap;
031import java.util.LinkedHashSet;
032import java.util.List;
033import java.util.Map;
034import java.util.ResourceBundle;
035import java.util.ServiceLoader;
036import java.util.Set;
037import java.util.TreeSet;
038import java.util.stream.Collector;
039import java.util.stream.Stream;
040import java.util.stream.StreamSupport;
041
042import org.apache.xbean.finder.AnnotationFinder;
043import org.talend.sdk.component.api.component.Components;
044import org.talend.sdk.component.api.component.Icon;
045import org.talend.sdk.component.runtime.manager.ParameterMeta;
046import org.talend.sdk.component.runtime.manager.reflect.ParameterModelService;
047import org.talend.sdk.component.runtime.manager.reflect.parameterenricher.BaseParameterEnricher;
048import org.talend.sdk.component.runtime.manager.service.LocalConfigurationService;
049import org.talend.sdk.component.runtime.manager.xbean.registry.EnrichedPropertyEditorRegistry;
050import org.talend.sdk.component.tools.spi.ValidationExtension;
051import org.talend.sdk.component.tools.validator.Validators;
052import org.talend.sdk.component.tools.validator.Validators.ValidatorHelper;
053
054import lombok.Data;
055
056// IMPORTANT: this class is used by reflection in gradle integration, don't break signatures without checking it
057public class ComponentValidator extends BaseTask {
058
059    public static final String ICONS = "icons" + File.separator;
060
061    private final Configuration configuration;
062
063    private final Log log;
064
065    private final ParameterModelService parameterModelService =
066            new ParameterModelService(new EnrichedPropertyEditorRegistry());
067
068    private final SvgValidator validator;
069
070    private final Map<Class<?>, List<ParameterMeta>> parametersCache = new HashMap<>();
071
072    private final List<ValidationExtension> extensions;
073
074    private final File sourceRoot;
075
076    public ComponentValidator(final Configuration configuration, final File[] classes, final Object log,
077            final File sourceRoot) {
078        super(classes);
079        this.configuration = configuration;
080        this.sourceRoot = sourceRoot;
081        this.validator = new SvgValidator(this.configuration.isValidateLegacyIcons());
082
083        try {
084            this.log = Log.class.isInstance(log) ? Log.class.cast(log) : new ReflectiveLog(log);
085        } catch (final NoSuchMethodException e) {
086            throw new IllegalArgumentException(e);
087        }
088        this.extensions = StreamSupport
089                .stream(ServiceLoader.load(ValidationExtension.class).spliterator(), false)
090                .collect(toList());
091    }
092
093    @Override
094    public void run() {
095        final AnnotationFinder finder = newFinder();
096        final List<Class<?>> components = ComponentHelper
097                .componentMarkers()
098                .flatMap(a -> finder.findAnnotatedClasses(a).stream())
099                .collect(toList());
100        components.forEach(c -> log.debug("Found component: " + c));
101
102        final Set<String> errors = new LinkedHashSet<>();
103        final Validators.ValidatorHelper helper = new ValidatorHelper() {
104
105            @Override
106            public boolean isService(final Parameter parameter) {
107                return ComponentValidator.this.parameterModelService
108                        .isService(new ParameterModelService.Param(parameter));
109            }
110
111            @Override
112            public ResourceBundle findResourceBundle(final Class<?> component) {
113                return ComponentValidator.this.findResourceBundle(component);
114            }
115
116            @Override
117            public String validateFamilyI18nKey(final Class<?> clazz, final String... keys) {
118                return ComponentValidator.this.validateFamilyI18nKey(clazz, keys);
119            }
120
121            @Override
122            public List<ParameterMeta> buildOrGetParameters(final Class<?> c) {
123                return ComponentValidator.this.buildOrGetParameters(c);
124            }
125
126            @Override
127            public String validateIcon(final Icon annotation, final Collection<String> errors) {
128                return ComponentValidator.this.validateIcon(annotation, errors);
129            }
130
131            @Override
132            public ParameterModelService getParameterModelService() {
133                return ComponentValidator.this.parameterModelService;
134            }
135
136            @Override
137            public Stream<File> componentClassFiles() {
138                if (ComponentValidator.this.classes == null) {
139                    return Stream.empty();
140                }
141                return Stream.of(ComponentValidator.this.classes);
142            }
143        };
144
145        final Validators validators = Validators.build(configuration, helper, extensions, sourceRoot);
146        final Set<String> errorsFromValidator = validators.validate(finder, components);
147        errors.addAll(errorsFromValidator);
148
149        if (!errors.isEmpty()) {
150            final List<String> preparedErrors =
151                    errors.stream().map(it -> it.replace("java.lang.", "").replace("java.util.", "")).collect(toList());
152            preparedErrors.forEach(log::error);
153            throw new IllegalStateException(
154                    "Some error were detected:" + preparedErrors.stream().collect(joining("\n- ", "\n- ", "")));
155        }
156
157        log.info("Validated components: " + components.stream().map(Class::getSimpleName).collect(joining(", ")));
158    }
159
160    private String validateIcon(final Icon annotation, final Collection<String> errors) {
161        if (classes.length == 0) {
162            return null;
163        }
164
165        if (annotation.value() == Icon.IconType.CUSTOM) {
166            final String icon = annotation.custom();
167            Set<File> svgs;
168            // legacy checks
169            if (configuration.isValidateLegacyIcons()) {
170                svgs = of(classes)
171                        .map(it -> new File(it, ICONS + icon + ".svg"))
172                        .collect(toSet());
173            } else {
174                // themed icons check
175                List<String> prefixes = new ArrayList<>();
176                of(classes).forEach(s -> {
177                    prefixes.add(s + File.separator + ICONS + "light" + File.separator + icon);
178                    prefixes.add(s + File.separator + ICONS + "dark" + File.separator + icon);
179                });
180                svgs = prefixes.stream().map(s -> new File(s + ".svg")).collect(toSet());
181            }
182
183            if (configuration.isValidateSvg()) {
184                errors.addAll(svgs.stream().filter(File::exists).flatMap(this::validateSvg).collect(toSet()));
185            }
186
187            List<File> missingSvgs = svgs.stream().filter(f -> !f.exists()).collect(toList());
188            if (!missingSvgs.isEmpty()) {
189                errors.addAll(missingSvgs.stream()
190                        .map(p -> String.format(
191                                "No '%s' found.",
192                                stripPath(p)))
193                        .collect(toList()));
194                return "Missing icon(s) in resources:";
195            }
196        }
197        return null;
198    }
199
200    private String stripPath(final File icon) {
201        try {
202            return icon.toString().substring(icon.toString().indexOf(ICONS));
203        } catch (StringIndexOutOfBoundsException e) {
204            log.error("Validate Icon Path Error :" + icon.toString() + "-- Exception: " + e.getMessage());
205        }
206        return ("Icon Path Error :" + icon.toString());
207    }
208
209    private Stream<String> validateSvg(final File file) {
210        return validator.validate(file.toPath());
211    }
212
213    private List<ParameterMeta> buildOrGetParameters(final Class<?> c) {
214        return parametersCache
215                .computeIfAbsent(c,
216                        k -> parameterModelService
217                                .buildParameterMetas(findConstructor(c),
218                                        ofNullable(c.getPackage()).map(Package::getName).orElse(""),
219                                        new BaseParameterEnricher.Context(
220                                                new LocalConfigurationService(emptyList(), "tools"))));
221    }
222
223    private String validateFamilyI18nKey(final Class<?> clazz, final String... keys) {
224        final Class<?> pck =
225                ComponentHelper.findPackageOrFail(clazz, apiTester(Components.class), Components.class.getName());
226        final String family = pck.getAnnotation(Components.class).family();
227        final String baseName = ofNullable(pck.getPackage()).map(p -> p.getName() + ".").orElse("") + "Messages";
228        final ResourceBundle bundle = findResourceBundle(pck);
229        if (bundle == null) {
230            return "No resource bundle for " + clazz.getName() + " translations, you should create a "
231                    + baseName.replace('.', '/') + ".properties at least.";
232        }
233
234        final Collection<String> missingKeys = of(keys)
235                .map(key -> key.replace("${family}", family))
236                .filter(k -> !bundle.containsKey(k))
237                .collect(toList());
238        if (!missingKeys.isEmpty()) {
239            return baseName + " is missing the key(s): " + String.join("\n", missingKeys);
240        }
241        return null;
242    }
243
244    private static <T> Collector<T, ?, Set<T>> toSet() {
245        return toCollection(TreeSet::new);
246    }
247
248    @Data
249    public static class Configuration {
250
251        private boolean validateFamily;
252
253        private boolean validateSerializable;
254
255        private boolean validateInternationalization;
256
257        private boolean validateInternationalizationAutoFix;
258
259        private boolean validateHttpClient;
260
261        private boolean validateModel;
262
263        private boolean validateMetadata;
264
265        private boolean validateComponent;
266
267        private boolean validateDataStore;
268
269        private boolean validateDataSet;
270
271        private boolean validateActions;
272
273        private boolean validateDocumentation;
274
275        private boolean validateWording;
276
277        private boolean validateLayout;
278
279        private boolean validateOptionNames;
280
281        private boolean validateLocalConfiguration;
282
283        private boolean validateOutputConnection;
284
285        private boolean validatePlaceholder;
286
287        private boolean validateSvg;
288
289        private boolean validateLegacyIcons;
290
291        private boolean validateNoFinalOption;
292
293        private String pluginId;
294
295        private boolean validateExceptions;
296
297        private boolean failOnValidateExceptions;
298
299        private boolean validateRecord;
300
301        private boolean validateSchema;
302
303        private boolean validateFixedSchema;
304
305    }
306}