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}