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.runtime.manager.reflect; 017 018import static java.util.Collections.emptyList; 019import static java.util.Collections.singletonList; 020import static java.util.Optional.ofNullable; 021import static java.util.stream.Collectors.toList; 022import static java.util.stream.Collectors.toMap; 023import static java.util.stream.Collectors.toSet; 024 025import java.lang.annotation.Annotation; 026import java.lang.reflect.AnnotatedElement; 027import java.lang.reflect.Executable; 028import java.lang.reflect.Field; 029import java.lang.reflect.Modifier; 030import java.lang.reflect.Parameter; 031import java.lang.reflect.ParameterizedType; 032import java.lang.reflect.Type; 033import java.util.ArrayList; 034import java.util.Arrays; 035import java.util.Collection; 036import java.util.Comparator; 037import java.util.HashMap; 038import java.util.HashSet; 039import java.util.List; 040import java.util.Map; 041import java.util.ServiceLoader; 042import java.util.Set; 043import java.util.Spliterator; 044import java.util.Spliterators; 045import java.util.stream.Stream; 046import java.util.stream.StreamSupport; 047 048import org.apache.xbean.propertyeditor.PropertyEditorRegistry; 049import org.talend.sdk.component.api.configuration.Option; 050import org.talend.sdk.component.api.configuration.constraint.Max; 051import org.talend.sdk.component.api.configuration.constraint.Min; 052import org.talend.sdk.component.api.configuration.ui.widget.DateTime; 053import org.talend.sdk.component.api.internationalization.Internationalized; 054import org.talend.sdk.component.api.service.Service; 055import org.talend.sdk.component.api.service.configuration.Configuration; 056import org.talend.sdk.component.api.service.http.Request; 057import org.talend.sdk.component.runtime.manager.ParameterMeta; 058import org.talend.sdk.component.runtime.manager.reflect.parameterenricher.BaseParameterEnricher; 059import org.talend.sdk.component.spi.parameter.ParameterExtensionEnricher; 060 061public class ParameterModelService { 062 063 private static final Annotation[] NO_ANNOTATIONS = new Annotation[0]; 064 065 private final Collection<ParameterExtensionEnricher> enrichers; 066 067 private final PropertyEditorRegistry registry; 068 069 private final Map<Type, Collection<Annotation>> implicitAnnotationsMapping; 070 071 protected ParameterModelService(final Collection<ParameterExtensionEnricher> enrichers, 072 final PropertyEditorRegistry registry) { 073 this.enrichers = enrichers; 074 this.registry = registry; 075 this.implicitAnnotationsMapping = enrichers 076 .stream() 077 .flatMap(enricher -> enricher.getImplicitAnnotationForTypes().entrySet().stream()) 078 .filter(e -> e.getValue() != null && !e.getValue().isEmpty()) 079 .collect(toMap(Map.Entry::getKey, Map.Entry::getValue, 080 (l1, l2) -> Stream.concat(l1.stream(), l2.stream()).collect(toSet()))); 081 } 082 083 public ParameterModelService(final PropertyEditorRegistry registry) { 084 this(StreamSupport 085 .stream(Spliterators 086 .spliteratorUnknownSize(ServiceLoader.load(ParameterExtensionEnricher.class).iterator(), 087 Spliterator.IMMUTABLE), 088 false) 089 .collect(toList()), registry); 090 } 091 092 public boolean isService(final Param parameter) { 093 final Class<?> type; 094 if (Class.class.isInstance(parameter.type)) { 095 type = Class.class.cast(parameter.type); 096 } else if (ParameterizedType.class.isInstance(parameter.type)) { 097 final Type rawType = ParameterizedType.class.cast(parameter.type).getRawType(); 098 if (Class.class.isInstance(rawType)) { 099 type = Class.class.cast(rawType); 100 } else { 101 return false; 102 } 103 } else { 104 return false; 105 } 106 return !parameter.isAnnotationPresent(Option.class) && (type.isAnnotationPresent(Service.class) 107 || parameter.isAnnotationPresent(Configuration.class) 108 || type.isAnnotationPresent(Internationalized.class) 109 || Stream.of(type.getMethods()).anyMatch(m -> m.isAnnotationPresent(Request.class)) 110 || (type.getName().startsWith("org.talend.sdk.component.") && type.getName().contains(".service.")) 111 || type.getName().startsWith("javax.")); 112 } 113 114 public List<ParameterMeta> buildParameterMetas(final Stream<Param> parameters, final Class<?> declaringClass, 115 final String i18nPackage, final boolean ignoreI18n, final BaseParameterEnricher.Context context) { 116 return parameters.filter(p -> !isService(p)).map(parameter -> { 117 final String name = findName(parameter, parameter.name); 118 return buildParameter(name, name, new ParameterMeta.Source() { 119 120 @Override 121 public String name() { 122 return parameter.name; 123 } 124 125 @Override 126 public Class<?> declaringClass() { 127 return declaringClass; 128 } 129 }, parameter.type, 130 Stream 131 .concat(extractTypeAnnotation(parameter), Stream.of(parameter.getAnnotations())) 132 .distinct() 133 .toArray(Annotation[]::new), 134 new ArrayList<>(singletonList(i18nPackage)), ignoreI18n, context); 135 }).collect(toList()); 136 } 137 138 private Stream<Annotation> extractTypeAnnotation(final Param parameter) { 139 if (Class.class.isInstance(parameter.type)) { 140 return Stream.of(Class.class.cast(parameter.type).getAnnotations()); 141 } 142 if (ParameterizedType.class.isInstance(parameter.type)) { 143 final ParameterizedType parameterizedType = ParameterizedType.class.cast(parameter.type); 144 if (Class.class.isInstance(parameterizedType.getRawType())) { 145 return Stream.of(Class.class.cast(parameterizedType.getRawType()).getAnnotations()); 146 } 147 } 148 return Stream.empty(); 149 } 150 151 private List<ParameterMeta> doBuildParameterMetas(final Executable executable, final String i18nPackage, 152 final boolean ignoreI18n, final BaseParameterEnricher.Context context) { 153 return buildParameterMetas(Stream.of(executable.getParameters()).map(Param::new), 154 executable.getDeclaringClass(), i18nPackage, ignoreI18n, context); 155 } 156 157 public List<ParameterMeta> buildServiceParameterMetas(final Executable executable, final String i18nPackage, 158 final BaseParameterEnricher.Context context) { 159 return doBuildParameterMetas(executable, i18nPackage, true, context); 160 } 161 162 public List<ParameterMeta> buildParameterMetas(final Executable executable, final String i18nPackage, 163 final BaseParameterEnricher.Context context) { 164 return doBuildParameterMetas(executable, i18nPackage, false, context); 165 } 166 167 protected ParameterMeta buildParameter(final String name, final String prefix, final ParameterMeta.Source source, 168 final Type genericType, final Annotation[] annotations, final Collection<String> i18nPackages, 169 final boolean ignoreI18n, final BaseParameterEnricher.Context context) { 170 final ParameterMeta.Type type = findType(genericType); 171 final String normalizedPrefix = prefix.endsWith(".") ? prefix.substring(0, prefix.length() - 1) : prefix; 172 final List<ParameterMeta> nested = new ArrayList<>(); 173 final List<String> proposals = new ArrayList<>(); 174 switch (type) { 175 case OBJECT: 176 addI18nPackageIfPossible(i18nPackages, genericType); 177 final List<ParameterMeta> meta = buildParametersMetas(name, normalizedPrefix + ".", genericType, 178 annotations, i18nPackages, ignoreI18n, context); 179 meta.sort(Comparator.comparing(ParameterMeta::getName)); 180 nested.addAll(meta); 181 break; 182 case ARRAY: 183 final Type nestedType = Class.class.isInstance(genericType) && Class.class.cast(genericType).isArray() 184 ? Class.class.cast(genericType).getComponentType() 185 : ParameterizedType.class.cast(genericType).getActualTypeArguments()[0]; 186 addI18nPackageIfPossible(i18nPackages, nestedType); 187 nested 188 .addAll(buildParametersMetas(name + "[${index}]", normalizedPrefix + "[${index}].", nestedType, 189 Class.class.isInstance(nestedType) ? Class.class.cast(nestedType).getAnnotations() 190 : NO_ANNOTATIONS, 191 i18nPackages, ignoreI18n, context)); 192 break; 193 case ENUM: 194 addI18nPackageIfPossible(i18nPackages, genericType); 195 proposals 196 .addAll(Stream 197 .of(((Class<? extends Enum<?>>) genericType).getEnumConstants()) 198 .map(Enum::name) 199 // sorted() // don't sort, let the dev use the order he wants 200 .collect(toList())); 201 break; 202 default: 203 } 204 // don't sort here to ensure we don't mess up parameter method ordering 205 return new ParameterMeta(source, genericType, type, normalizedPrefix, name, 206 i18nPackages.toArray(new String[i18nPackages.size()]), nested, proposals, 207 buildExtensions(name, genericType, annotations, context), false); 208 } 209 210 private void addI18nPackageIfPossible(final Collection<String> i18nPackages, final Type type) { 211 if (Class.class.isInstance(type)) { 212 final Package typePck = Class.class.cast(type).getPackage(); 213 if (typePck != null && !typePck.getName().isEmpty() && !i18nPackages.contains(typePck.getName())) { 214 i18nPackages.add(typePck.getName()); 215 } 216 } 217 } 218 219 private Map<String, String> buildExtensions(final String name, final Type genericType, 220 final Annotation[] annotations, final BaseParameterEnricher.Context context) { 221 return getAnnotations(genericType, annotations).distinct().flatMap(a -> enrichers.stream().map(e -> { 222 if (BaseParameterEnricher.class.isInstance(e)) { 223 final BaseParameterEnricher bpe = BaseParameterEnricher.class.cast(e); 224 return bpe.withContext(context, () -> bpe.onParameterAnnotation(name, genericType, a)); 225 } 226 return e.onParameterAnnotation(name, genericType, a); 227 })).flatMap(map -> map.entrySet().stream()).collect(toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> { 228 if (a.equals(b)) { 229 return a; 230 } 231 throw new IllegalArgumentException("Ambiguous metadata: '" + a + "'/'" + b + "'"); 232 })); 233 } 234 235 private Stream<Annotation> getAnnotations(final Type type, final Annotation[] annotations) { 236 // we have a few constraints that are defined by implicit annotations. those constraints can be overwritten by 237 // explicit annotations. 238 // to avoid ambiguous exception we should filter out constraints that are provided by implicit annotation 239 final Set<Class<? extends Annotation>> overwrittableAnnotations = 240 new HashSet<>(Arrays.asList(Min.class, Max.class, DateTime.class)); 241 final Set<? extends Class<? extends Annotation>> skipImplicit = Stream.of(annotations) 242 .map(Annotation::annotationType) 243 .filter(overwrittableAnnotations::contains) 244 .collect(toSet()); 245 return Stream 246 .concat(getReflectionAnnotations(type, annotations), 247 implicitAnnotationsMapping.getOrDefault(type, emptyList()) 248 .stream() 249 .filter(it -> !skipImplicit.contains(it.annotationType()))); 250 } 251 252 private Stream<Annotation> getReflectionAnnotations(final Type genericType, final Annotation[] annotations) { 253 return Stream 254 .concat(Stream.of(annotations), 255 // if a class concat its annotations 256 Class.class 257 .isInstance(genericType) 258 ? getClassAnnotations(genericType, annotations) 259 : (hasAClassFirstParameter(genericType) ? getClassAnnotations( 260 ParameterizedType.class.cast(genericType).getActualTypeArguments()[0], 261 annotations) : Stream.empty())); 262 } 263 264 private boolean hasAClassFirstParameter(final Type genericType) { 265 return ParameterizedType.class.isInstance(genericType) // if a list concat the item type annotations 266 && ParameterizedType.class.cast(genericType).getActualTypeArguments().length == 1 267 && Class.class.isInstance(ParameterizedType.class.cast(genericType).getActualTypeArguments()[0]); 268 } 269 270 private Stream<Annotation> getClassAnnotations(final Type genericType, final Annotation[] annotations) { 271 return Stream 272 .of(Class.class.cast(genericType).getAnnotations()) 273 .filter(a -> Stream.of(annotations).noneMatch(o -> o.annotationType() == a.annotationType())); 274 } 275 276 private List<ParameterMeta> buildParametersMetas(final String name, final String prefix, final Type type, 277 final Annotation[] annotations, final Collection<String> i18nPackages, final boolean ignoreI18n, 278 final BaseParameterEnricher.Context context) { 279 if (ParameterizedType.class.isInstance(type)) { 280 final ParameterizedType pt = ParameterizedType.class.cast(type); 281 if (!Class.class.isInstance(pt.getRawType())) { 282 throw new IllegalArgumentException("Unsupported raw type in ParameterizedType parameter: " + pt); 283 } 284 final Class<?> raw = Class.class.cast(pt.getRawType()); 285 if (Collection.class.isAssignableFrom(raw)) { 286 if (!Class.class.isInstance(pt.getActualTypeArguments()[0])) { 287 throw new IllegalArgumentException( 288 "Unsupported list: " + pt + ", ensure to use a concrete class as generic"); 289 } 290 return buildParametersMetas(name, prefix, Class.class.cast(type), annotations, i18nPackages, ignoreI18n, 291 context); 292 } 293 if (Map.class.isAssignableFrom(raw)) { 294 if (!Class.class.isInstance(pt.getActualTypeArguments()[0]) 295 || !Class.class.isInstance(pt.getActualTypeArguments()[1])) { 296 throw new IllegalArgumentException( 297 "Unsupported map: " + pt + ", ensure to use a concrete class as generics"); 298 } 299 return Stream 300 .concat(buildParametersMetas(name + ".key[${index}]", prefix + "key[${index}].", 301 Class.class.cast(pt.getActualTypeArguments()[0]), annotations, i18nPackages, ignoreI18n, 302 context).stream(), 303 buildParametersMetas(name + ".value[${index}]", prefix + "value[${index}].", 304 Class.class.cast(pt.getActualTypeArguments()[1]), annotations, i18nPackages, 305 ignoreI18n, context).stream()) 306 .collect(toList()); 307 } 308 } 309 if (Class.class.isInstance(type)) { 310 switch (findType(type)) { 311 case ENUM: 312 case STRING: 313 case NUMBER: 314 case BOOLEAN: 315 return singletonList(buildParameter(name, prefix, new ParameterMeta.Source() { 316 317 @Override 318 public String name() { 319 return name; 320 } 321 322 @Override 323 public Class<?> declaringClass() { 324 return Class.class.cast(type); 325 } 326 }, type, annotations, i18nPackages, ignoreI18n, context)); 327 default: 328 } 329 return buildObjectParameters(prefix, Class.class.cast(type), i18nPackages, ignoreI18n, context); 330 } 331 throw new IllegalArgumentException("Unsupported parameter type: " + type); 332 } 333 334 private List<ParameterMeta> buildObjectParameters(final String prefix, final Class<?> type, 335 final Collection<String> i18nPackages, final boolean ignoreI18n, 336 final BaseParameterEnricher.Context context) { 337 final Map<String, Field> fields = new HashMap<>(); 338 final List<ParameterMeta> out = new ArrayList<>(); 339 Class<?> current = type; 340 while (current != null && current != Object.class) { 341 out 342 .addAll(Stream 343 .of(current.getDeclaredFields()) 344 .filter(f -> f.isAnnotationPresent(Option.class)) 345 .filter(f -> !"$jacocoData".equals(f.getName()) && !Modifier.isStatic(f.getModifiers()) 346 && (f.getModifiers() & 0x00001000/* SYNTHETIC */) == 0) 347 .filter(f -> fields.putIfAbsent(f.getName(), f) == null) 348 .map(f -> { 349 final String name = findName(f, f.getName()); 350 final String path = prefix + name; 351 return buildParameter(name, path + ".", new ParameterMeta.Source() { 352 353 @Override 354 public String name() { 355 return f.getName(); 356 } 357 358 @Override 359 public Class<?> declaringClass() { 360 return f.getDeclaringClass(); 361 } 362 }, f.getGenericType(), f.getAnnotations(), i18nPackages, ignoreI18n, context); 363 }) 364 .collect(toList())); 365 current = current.getSuperclass(); 366 } 367 return out; 368 } 369 370 public String findName(final AnnotatedElement parameter, final String defaultName) { 371 return ofNullable(parameter.getAnnotation(Option.class)) 372 .map(Option::value) 373 .filter(v -> !v.isEmpty()) 374 .orElse(defaultName); 375 } 376 377 private ParameterMeta.Type findType(final Type type) { 378 if (Class.class.isInstance(type)) { 379 final Class<?> clazz = Class.class.cast(type); 380 381 // we handled char before so we only have numbers now for primitives 382 if (Primitives.unwrap(clazz) == boolean.class) { 383 return ParameterMeta.Type.BOOLEAN; 384 } 385 if (Primitives.unwrap(clazz) == char.class) { 386 return ParameterMeta.Type.STRING; 387 } 388 if (clazz.isPrimitive() || Primitives.unwrap(clazz) != clazz) { 389 return ParameterMeta.Type.NUMBER; 390 } 391 392 if (clazz.isEnum()) { 393 return ParameterMeta.Type.ENUM; 394 } 395 if (clazz.isArray()) { 396 return ParameterMeta.Type.ARRAY; 397 } 398 } 399 if (ParameterizedType.class.isInstance(type)) { 400 final ParameterizedType pt = ParameterizedType.class.cast(type); 401 if (Class.class.isInstance(pt.getRawType())) { 402 final Class<?> raw = Class.class.cast(pt.getRawType()); 403 if (Collection.class.isAssignableFrom(raw)) { 404 return ParameterMeta.Type.ARRAY; 405 } 406 if (Map.class.isAssignableFrom(raw)) { 407 return ParameterMeta.Type.OBJECT; 408 } 409 } 410 throw new IllegalArgumentException("Unsupported type: " + pt); 411 } 412 if (StringCompatibleTypes.isKnown(type, registry)) { // flatten the config as a string 413 return ParameterMeta.Type.STRING; 414 } 415 return ParameterMeta.Type.OBJECT; 416 } 417 418 public static class Param implements AnnotatedElement { 419 420 private final Type type; 421 422 private final String name; 423 424 private final Annotation[] annotations; 425 426 public Param(final Type type, final Annotation[] annotations, final String name) { 427 this.type = type; 428 this.annotations = annotations; 429 this.name = name; 430 } 431 432 public Param(final Parameter parameter) { 433 this(parameter.getParameterizedType(), parameter.getAnnotations(), parameter.getName()); 434 } 435 436 @Override 437 public <T extends Annotation> T getAnnotation(final Class<T> annotationClass) { 438 return Stream 439 .of(getAnnotations()) 440 .filter(it -> it.annotationType() == annotationClass) 441 .findFirst() 442 .map(annotationClass::cast) 443 .orElse(null); 444 } 445 446 @Override 447 public Annotation[] getAnnotations() { 448 return annotations; 449 } 450 451 @Override 452 public Annotation[] getDeclaredAnnotations() { 453 return getAnnotations(); 454 } 455 } 456}