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}