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