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}