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.Optional.ofNullable;
019import static java.util.function.Function.identity;
020import static java.util.stream.Collectors.toConcurrentMap;
021import static java.util.stream.Collectors.toList;
022import static java.util.stream.Collectors.toMap;
023import static java.util.stream.Collectors.toSet;
024import static org.talend.sdk.component.runtime.manager.reflect.Constructors.findConstructor;
025
026import java.beans.ConstructorProperties;
027import java.io.Reader;
028import java.io.StringReader;
029import java.lang.annotation.Annotation;
030import java.lang.reflect.Array;
031import java.lang.reflect.Constructor;
032import java.lang.reflect.Executable;
033import java.lang.reflect.Field;
034import java.lang.reflect.ParameterizedType;
035import java.lang.reflect.Type;
036import java.util.AbstractMap;
037import java.util.ArrayList;
038import java.util.Collection;
039import java.util.Collections;
040import java.util.Comparator;
041import java.util.HashMap;
042import java.util.HashSet;
043import java.util.LinkedHashMap;
044import java.util.List;
045import java.util.Locale;
046import java.util.Map;
047import java.util.Set;
048import java.util.concurrent.ConcurrentHashMap;
049import java.util.concurrent.ConcurrentMap;
050import java.util.function.BiFunction;
051import java.util.function.BinaryOperator;
052import java.util.function.Function;
053import java.util.function.Predicate;
054import java.util.function.Supplier;
055import java.util.stream.Collector;
056import java.util.stream.Stream;
057
058import javax.json.Json;
059import javax.json.JsonArray;
060import javax.json.JsonNumber;
061import javax.json.JsonObject;
062import javax.json.JsonReader;
063import javax.json.JsonReaderFactory;
064import javax.json.JsonString;
065import javax.json.JsonValue;
066import javax.json.spi.JsonProvider;
067
068import org.apache.xbean.propertyeditor.PropertyEditorRegistry;
069import org.apache.xbean.recipe.ObjectRecipe;
070import org.apache.xbean.recipe.UnsetPropertiesRecipe;
071import org.mozilla.javascript.Context;
072import org.mozilla.javascript.Scriptable;
073import org.talend.sdk.component.api.record.Schema;
074import org.talend.sdk.component.api.service.configuration.Configuration;
075import org.talend.sdk.component.api.service.configuration.LocalConfiguration;
076import org.talend.sdk.component.runtime.internationalization.InternationalizationServiceFactory;
077import org.talend.sdk.component.runtime.manager.ParameterMeta;
078import org.talend.sdk.component.runtime.manager.reflect.parameterenricher.BaseParameterEnricher;
079import org.talend.sdk.component.runtime.manager.reflect.visibility.PayloadMapper;
080import org.talend.sdk.component.runtime.manager.reflect.visibility.VisibilityService;
081
082import lombok.RequiredArgsConstructor;
083import lombok.extern.slf4j.Slf4j;
084
085@Slf4j
086@RequiredArgsConstructor
087public class ReflectionService {
088
089    private final ParameterModelService parameterModelService;
090
091    private final PropertyEditorRegistry propertyEditorRegistry;
092
093    // note: we use xbean for now but we can need to add some caching inside if we
094    // abuse of it at runtime.
095    // not a concern for now.
096    //
097    // note2: compared to {@link ParameterModelService}, here we build the instance
098    // and we start from the config and not the
099    // model.
100    //
101    // IMPORTANT: ensure to be able to read all data (including collection) from a
102    // map to support system properties override
103    public Function<Map<String, String>, Object[]> parameterFactory(final Executable executable,
104            final Map<Class<?>, Object> precomputed, final List<ParameterMeta> metas) {
105        final ClassLoader loader = Thread.currentThread().getContextClassLoader();
106        final Function<Supplier<Object>, Object> contextualSupplier = createContextualSupplier(loader);
107        final Collection<Function<Map<String, String>, Object>> factories =
108                Stream.of(executable.getParameters()).map(parameter -> {
109                    final String name = parameterModelService.findName(parameter, parameter.getName());
110                    final Type parameterizedType = parameter.getParameterizedType();
111                    if (Class.class.isInstance(parameterizedType)) {
112                        if (parameter.isAnnotationPresent(Configuration.class)) {
113                            try {
114                                final Class configClass = Class.class.cast(parameterizedType);
115                                return createConfigFactory(precomputed, loader, contextualSupplier, parameter.getName(),
116                                        parameter.getAnnotation(Configuration.class), parameter.getAnnotations(),
117                                        configClass);
118                            } catch (final NoSuchMethodException e) {
119                                throw new IllegalArgumentException("No constructor for " + parameter);
120                            }
121                        }
122                        final Object value = precomputed.get(parameterizedType);
123                        if (value != null) {
124                            if (Copiable.class.isInstance(value)) {
125                                final Copiable copiable = Copiable.class.cast(value);
126                                return (Function<Map<String, String>, Object>) config -> copiable.copy(value);
127                            }
128                            return (Function<Map<String, String>, Object>) config -> value;
129                        }
130                        final BiFunction<String, Map<String, Object>, Object> objectFactory = createObjectFactory(
131                                loader, contextualSupplier, parameterizedType, translate(metas, name), precomputed);
132                        return (Function<Map<String, String>, Object>) config -> objectFactory
133                                .apply(name, Map.class.cast(config));
134                    }
135
136                    if (ParameterizedType.class.isInstance(parameterizedType)) {
137                        final ParameterizedType pt = ParameterizedType.class.cast(parameterizedType);
138                        if (Class.class.isInstance(pt.getRawType())) {
139                            if (Collection.class.isAssignableFrom(Class.class.cast(pt.getRawType()))) {
140                                final Class<?> collectionType = Class.class.cast(pt.getRawType());
141                                final Type itemType = pt.getActualTypeArguments()[0];
142                                if (!Class.class.isInstance(itemType)) {
143                                    throw new IllegalArgumentException(
144                                            "For now we only support Collection<T> with T a Class<?>");
145                                }
146                                final Class<?> itemClass = Class.class.cast(itemType);
147
148                                // if we have services matching this type then we return the collection of
149                                // services, otherwise
150                                // we consider it is a config
151                                final Collection<Object> services = precomputed
152                                        .entrySet()
153                                        .stream()
154                                        .sorted(Comparator.comparing(e -> e.getKey().getName()))
155                                        .filter(e -> itemClass.isAssignableFrom(e.getKey()))
156                                        .map(Map.Entry::getValue)
157                                        .collect(toList());
158
159                                // let's try to catch up built-in service
160                                // here we have 1 entry per type and it is lazily created on get()
161                                if (services.isEmpty()) {
162                                    final Object o = precomputed.get(itemClass);
163                                    if (o != null) {
164                                        services.add(o);
165                                    }
166                                }
167
168                                if (!services.isEmpty()) {
169                                    return (Function<Map<String, String>, Object>) config -> services;
170                                }
171
172                                // here we know we just want to instantiate a config list and not services
173                                final Collector collector = Set.class == collectionType ? toSet() : toList();
174                                final List<ParameterMeta> parameterMetas = translate(metas, name);
175                                final BiFunction<String, Map<String, Object>, Object> itemFactory = createObjectFactory(
176                                        loader, contextualSupplier, itemClass, parameterMetas, precomputed);
177                                return (Function<Map<String, String>, Object>) config -> createList(loader,
178                                        contextualSupplier, name, collectionType, itemClass, collector, itemFactory,
179                                        Map.class.cast(config), parameterMetas, precomputed);
180                            }
181                            if (Map.class.isAssignableFrom(Class.class.cast(pt.getRawType()))) {
182                                final Class<?> mapType = Class.class.cast(pt.getRawType());
183                                final Type keyItemType = pt.getActualTypeArguments()[0];
184                                final Type valueItemType = pt.getActualTypeArguments()[1];
185                                if (!Class.class.isInstance(keyItemType) || !Class.class.isInstance(valueItemType)) {
186                                    throw new IllegalArgumentException(
187                                            "For now we only support Map<A, B> with A and B a Class<?>");
188                                }
189                                final Class<?> keyItemClass = Class.class.cast(keyItemType);
190                                final Class<?> valueItemClass = Class.class.cast(valueItemType);
191                                final List<ParameterMeta> parameterMetas = translate(metas, name);
192                                final BiFunction<String, Map<String, Object>, Object> keyItemFactory =
193                                        createObjectFactory(loader, contextualSupplier, keyItemClass, parameterMetas,
194                                                precomputed);
195                                final BiFunction<String, Map<String, Object>, Object> valueItemFactory =
196                                        createObjectFactory(loader, contextualSupplier, valueItemClass, parameterMetas,
197                                                precomputed);
198                                final Collector collector =
199                                        createMapCollector(mapType, keyItemClass, valueItemClass, precomputed);
200                                return (Function<Map<String, String>, Object>) config -> createMap(name, mapType,
201                                        keyItemFactory, valueItemFactory, collector, Map.class.cast(config));
202                            }
203                        }
204                    }
205
206                    throw new IllegalArgumentException("Unsupported type: " + parameterizedType);
207                }).collect(toList());
208
209        return config -> {
210            final Map<String, String> notNullConfig = ofNullable(config).orElseGet(Collections::emptyMap);
211            final PayloadValidator visitor = new PayloadValidator();
212            if (!visitor.skip) {
213                visitor.globalPayload = new PayloadMapper((a, b) -> {
214                }).visitAndMap(metas, notNullConfig);
215                final PayloadMapper payloadMapper = new PayloadMapper(visitor);
216                payloadMapper.setGlobalPayload(visitor.globalPayload);
217                payloadMapper.visitAndMap(metas, notNullConfig);
218                visitor.throwIfFailed();
219            }
220            return factories.stream().map(f -> f.apply(notNullConfig)).toArray(Object[]::new);
221        };
222    }
223
224    public Function<Supplier<Object>, Object> createContextualSupplier(final ClassLoader loader) {
225        return supplier -> {
226            final Thread thread = Thread.currentThread();
227            final ClassLoader old = thread.getContextClassLoader();
228            thread.setContextClassLoader(loader);
229            try {
230                return supplier.get();
231            } finally {
232                thread.setContextClassLoader(old);
233            }
234        };
235    }
236
237    public Function<Map<String, String>, Object> createConfigFactory(final Map<Class<?>, Object> precomputed,
238            final ClassLoader loader, final Function<Supplier<Object>, Object> contextualSupplier, final String name,
239            final Configuration configuration, final Annotation[] allAnnotations, final Class<?> configClass)
240            throws NoSuchMethodException {
241        final Constructor constructor = configClass.getConstructor();
242        final LocalConfiguration config = LocalConfiguration.class.cast(precomputed.get(LocalConfiguration.class));
243        if (config == null) {
244            return c -> null;
245        }
246
247        final String prefix = configuration.value();
248        final ParameterMeta objectMeta =
249                parameterModelService.buildParameter(prefix, prefix, new ParameterMeta.Source() {
250
251                    @Override
252                    public String name() {
253                        return name;
254                    }
255
256                    @Override
257                    public Class<?> declaringClass() {
258                        return constructor.getDeclaringClass();
259                    }
260                }, configClass, allAnnotations, Stream
261                        .of(ofNullable(constructor.getDeclaringClass().getPackage()).map(Package::getName).orElse(""))
262                        .collect(toList()), true, new BaseParameterEnricher.Context(config));
263        final BiFunction<String, Map<String, Object>, Object> objectFactory = createObjectFactory(loader,
264                contextualSupplier, configClass, objectMeta.getNestedParameters(), precomputed);
265        final Function<Map<String, Object>, Object> factory = c -> objectFactory.apply(prefix, c);
266        return ignoredDependentConfig -> {
267            final Map<String, Object> configMap = config
268                    .keys()
269                    .stream()
270                    .filter(it -> objectMeta
271                            .getNestedParameters()
272                            .stream()
273                            .anyMatch(p -> it.startsWith(prefix + '.' + p.getName())))
274                    .collect(toMap(identity(), config::get));
275            return factory.apply(configMap);
276        };
277    }
278
279    private List<ParameterMeta> translate(final List<ParameterMeta> metas, final String name) {
280        if (metas == null) {
281            return null;
282        }
283        return metas
284                .stream()
285                .filter(it -> it.getName().equals(name))
286                .flatMap(it -> it.getNestedParameters().stream())
287                .collect(toList());
288    }
289
290    private Collector createMapCollector(final Class<?> mapType, final Class<?> keyItemClass,
291            final Class<?> valueItemClass, final Map<Class<?>, Object> precomputed) {
292        final Function<Map.Entry<?, ?>, Object> keyMapper = o -> doConvert(keyItemClass, o.getKey(), precomputed);
293        final Function<Map.Entry<?, ?>, Object> valueMapper = o -> doConvert(valueItemClass, o.getValue(), precomputed);
294        return ConcurrentMap.class.isAssignableFrom(mapType) ? toConcurrentMap(keyMapper, valueMapper)
295                : toMap(keyMapper, valueMapper);
296    }
297
298    private Object createList(final ClassLoader loader, final Function<Supplier<Object>, Object> contextualSupplier,
299            final String name, final Class<?> collectionType, final Class<?> itemClass, final Collector collector,
300            final BiFunction<String, Map<String, Object>, Object> itemFactory, final Map<String, Object> config,
301            final List<ParameterMeta> metas, final Map<Class<?>, Object> precomputed) {
302        final Object obj = config.get(name);
303        if (collectionType.isInstance(obj)) {
304            return Collection.class
305                    .cast(obj)
306                    .stream()
307                    .map(o -> doConvert(itemClass, o, precomputed))
308                    .collect(collector);
309        }
310
311        // try to build it from the properties
312        // <value>[<index>] = xxxxx
313        // <value>[<index>].<property> = xxxxx
314        final Collection collection = List.class.isAssignableFrom(collectionType) ? new ArrayList<>() : new HashSet<>();
315        final int maxLength = getArrayMaxLength(name, config);
316        int paramIdx = 0;
317        String[] args = null;
318        while (paramIdx < maxLength) {
319            final String configName = String.format("%s[%d]", name, paramIdx);
320            if (!config.containsKey(configName)) {
321                if (config.keySet().stream().anyMatch(k -> k.startsWith(configName + "."))) { // object
322                                                                                              // mapping
323                    if (paramIdx == 0) {
324                        args = findArgsName(itemClass);
325                    }
326                    collection
327                            .add(createObject(loader, contextualSupplier, itemClass, args, configName, config, metas,
328                                    precomputed));
329                } else {
330                    break;
331                }
332            } else {
333                collection.add(itemFactory.apply(configName, config));
334            }
335            paramIdx++;
336        }
337
338        return collection;
339    }
340
341    private Integer getArrayMaxLength(final String prefix, final Map<String, Object> config) {
342        return ofNullable(config.get(prefix + "[length]"))
343                .map(String::valueOf)
344                .map(Integer::parseInt)
345                .orElse(Integer.MAX_VALUE);
346    }
347
348    private Object createMap(final String name, final Class<?> mapType,
349            final BiFunction<String, Map<String, Object>, Object> keyItemFactory,
350            final BiFunction<String, Map<String, Object>, Object> valueItemFactory, final Collector collector,
351            final Map<String, Object> config) {
352        final Object obj = config.get(name);
353        if (mapType.isInstance(obj)) {
354            return Map.class.cast(obj).entrySet().stream().collect(collector);
355        }
356
357        // try to build it from the properties
358        // <value>.key[<index>] = xxxxx
359        // <value>.key[<index>].<property> = xxxxx
360        // <value>.value[<index>] = xxxxx
361        // <value>.value[<index>].<property> = xxxxx
362        final Map map = ConcurrentMap.class.isAssignableFrom(mapType) ? new ConcurrentHashMap() : new HashMap();
363        int paramIdx = 0;
364        do {
365            final String keyConfigName = String.format("%s.key[%d]", name, paramIdx);
366            final String valueConfigName = String.format("%s.value[%d]", name, paramIdx);
367            if (!config.containsKey(keyConfigName) || !config.containsKey(valueConfigName)) { // quick test first
368                if (config.keySet().stream().noneMatch(k -> k.startsWith(keyConfigName))
369                        && config.keySet().stream().noneMatch(k -> k.startsWith(valueConfigName))) {
370                    break;
371                }
372            }
373            map.put(keyItemFactory.apply(keyConfigName, config), valueItemFactory.apply(valueConfigName, config));
374            paramIdx++;
375        } while (true);
376
377        return map;
378    }
379
380    private BiFunction<String, Map<String, Object>, Object> createObjectFactory(final ClassLoader loader,
381            final Function<Supplier<Object>, Object> contextualSupplier, final Type type,
382            final List<ParameterMeta> metas, final Map<Class<?>, Object> precomputed) {
383        final Class clazz = Class.class.cast(type);
384        if (clazz.isPrimitive() || Primitives.unwrap(clazz) != clazz || String.class == clazz) {
385            return (name, config) -> doConvert(clazz, config.get(name), precomputed);
386        }
387        if (clazz.isEnum()) {
388            return (name, config) -> ofNullable(config.get(name))
389                    .map(String.class::cast)
390                    .map(String::trim)
391                    .filter(it -> !it.isEmpty())
392                    .map(v -> Enum.valueOf(clazz, v))
393                    .orElse(null);
394        }
395
396        final String[] args = findArgsName(clazz);
397        return (name, config) -> contextualSupplier
398                .apply(() -> createObject(loader, contextualSupplier, clazz, args, name, config, metas, precomputed));
399    }
400
401    private String[] findArgsName(final Class clazz) {
402        return Stream
403                .of(clazz.getConstructors())
404                .filter(c -> c.isAnnotationPresent(ConstructorProperties.class))
405                .findFirst()
406                .map(c -> ConstructorProperties.class.cast(c.getAnnotation(ConstructorProperties.class)).value())
407                .orElse(null);
408    }
409
410    private JsonValue createJsonValue(final Object value, final Map<Class<?>, Object> precomputed,
411            final Function<Reader, JsonReader> fallbackReaderCreator) {
412        final StringReader sr = new StringReader(String.valueOf(value).trim());
413        try (final JsonReader reader = ofNullable(precomputed.get(JsonReaderFactory.class))
414                .map(JsonReaderFactory.class::cast)
415                .map(f -> f.createReader(sr))
416                .orElseGet(() -> fallbackReaderCreator.apply(sr))) {
417            return reader.read();
418        }
419    }
420
421    private Object createObject(final ClassLoader loader, final Function<Supplier<Object>, Object> contextualSupplier,
422            final Class clazz, final String[] args, final String name, final Map<String, Object> config,
423            final List<ParameterMeta> metas, final Map<Class<?>, Object> precomputed) {
424        final Object potentialJsonValue = config.get(name);
425        if (JsonObject.class == clazz && String.class.isInstance(potentialJsonValue)) {
426            return createJsonValue(potentialJsonValue, precomputed, Json::createReader).asJsonObject();
427        }
428        if (propertyEditorRegistry.findConverter(clazz) != null && Schema.class.isAssignableFrom(clazz)) {
429            final Object configValue = config.get(name);
430            if (String.class.isInstance(configValue)) {
431                return propertyEditorRegistry.getValue(clazz, String.class.cast(configValue));
432            }
433        }
434        if (propertyEditorRegistry.findConverter(clazz) != null && config.size() == 1) {
435            final Object configValue = config.values().iterator().next();
436            if (String.class.isInstance(configValue)) {
437                return propertyEditorRegistry.getValue(clazz, String.class.cast(configValue));
438            }
439        }
440
441        final String prefix = name.isEmpty() ? "" : name + ".";
442        final ObjectRecipe recipe = newRecipe(clazz);
443        recipe.setProperty("rawProperties", new UnsetPropertiesRecipe()); // todo: log unused props?
444        ofNullable(args).ifPresent(recipe::setConstructorArgNames);
445
446        final Map<String, Object> specificMapping = config
447                .entrySet()
448                .stream()
449                .filter(e -> e.getKey().startsWith(prefix) || prefix.isEmpty())
450                .collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
451
452        // extract map configuration
453        final Map<String, Object> mapEntries = specificMapping.entrySet().stream().filter(e -> {
454            final String key = e.getKey();
455            final int idxStart = key.indexOf('[', prefix.length());
456            return idxStart > 0 && ((idxStart > ".key".length() && key.startsWith(".key", idxStart - ".key".length()))
457                    || (idxStart > ".value".length() && key.startsWith(".value", idxStart - ".value".length())));
458        })
459                .sorted(this::sortIndexEntry)
460                .collect(toMap(Map.Entry::getKey, Map.Entry::getValue, noMerge(), LinkedHashMap::new));
461        mapEntries.keySet().forEach(specificMapping::remove);
462        final Map<String, Object> preparedMaps = new HashMap<>();
463        for (final Map.Entry<String, Object> entry : mapEntries.entrySet()) {
464            final String key = entry.getKey();
465            final int idxStart = key.indexOf('[', prefix.length());
466            String enclosingName = key.substring(prefix.length(), idxStart);
467            if (enclosingName.endsWith(".key")) {
468                enclosingName = enclosingName.substring(0, enclosingName.length() - ".key".length());
469            } else if (enclosingName.endsWith(".value")) {
470                enclosingName = enclosingName.substring(0, enclosingName.length() - ".value".length());
471            } else {
472                throw new IllegalArgumentException("'" + key + "' is not supported, it is considered as a map binding");
473            }
474            if (preparedMaps.containsKey(enclosingName)) {
475                continue;
476            }
477            if (isUiParam(enclosingName)) { // normally cleaned up by the UI@back integration but safeguard here
478                continue;
479            }
480
481            final Type genericType =
482                    findField(normalizeName(enclosingName.substring(enclosingName.indexOf('.') + 1), metas), clazz)
483                            .getGenericType();
484            final ParameterizedType pt = validateObject(clazz, enclosingName, genericType);
485
486            final Class<?> keyType = Class.class.cast(pt.getActualTypeArguments()[0]);
487            final Class<?> valueType = Class.class.cast(pt.getActualTypeArguments()[1]);
488            preparedMaps
489                    .put(enclosingName, createMap(prefix + enclosingName, Map.class,
490                            createObjectFactory(loader, contextualSupplier, keyType, metas, precomputed),
491                            createObjectFactory(loader, contextualSupplier, valueType, metas, precomputed),
492                            createMapCollector(Class.class.cast(pt.getRawType()), keyType, valueType, precomputed),
493                            new HashMap<>(mapEntries)));
494        }
495
496        // extract list configuration
497        final Map<String, Object> listEntries = specificMapping.entrySet().stream().filter(e -> {
498            final String key = e.getKey();
499            final int idxStart = key.indexOf('[', prefix.length());
500            final int idxEnd = key.indexOf(']', prefix.length());
501            final int sep = key.indexOf('.', prefix.length() + 1);
502            return idxStart > 0 && key.endsWith("]") && (sep > idxEnd || sep < 0);
503        })
504                .sorted(this::sortIndexEntry)
505                .collect(toMap(Map.Entry::getKey, Map.Entry::getValue, noMerge(), LinkedHashMap::new));
506        listEntries.keySet().forEach(specificMapping::remove);
507        final Map<String, Object> preparedLists = new HashMap<>();
508        for (final Map.Entry<String, Object> entry : listEntries.entrySet()) {
509            final String key = entry.getKey();
510            final int idxStart = key.indexOf('[', prefix.length());
511            final String enclosingName = key.substring(prefix.length(), idxStart);
512            if (preparedLists.containsKey(enclosingName)) {
513                continue;
514            }
515            if (isUiParam(enclosingName)) {
516                continue;
517            }
518
519            final Type genericType = findField(normalizeName(enclosingName, metas), clazz).getGenericType();
520            if (Class.class.isInstance(genericType)) {
521                final Class<?> arrayClass = Class.class.cast(genericType);
522                if (arrayClass.isArray()) {
523                    // we could use Array.newInstance but for now use the list, shouldn't impact
524                    // much the perf
525                    final Collection<?> list = Collection.class
526                            .cast(createList(loader, contextualSupplier, prefix + enclosingName, List.class,
527                                    arrayClass.getComponentType(), toList(), createObjectFactory(loader,
528                                            contextualSupplier, arrayClass.getComponentType(), metas, precomputed),
529                                    new HashMap<>(listEntries), metas, precomputed));
530
531                    // we need that conversion to ensure the type matches
532                    final Object array = Array.newInstance(arrayClass.getComponentType(), list.size());
533                    int idx = 0;
534                    for (final Object o : list) {
535                        Array.set(array, idx++, o);
536                    }
537                    preparedLists.put(enclosingName, array);
538                    continue;
539                } // else let it fail with the "collection" error
540            }
541
542            // now we need an actual collection type
543            final ParameterizedType pt = validateCollection(clazz, enclosingName, genericType);
544            final Type itemType = pt.getActualTypeArguments()[0];
545            preparedLists
546                    .put(enclosingName,
547                            createList(loader, contextualSupplier, prefix + enclosingName,
548                                    Class.class.cast(pt.getRawType()), Class.class.cast(itemType), toList(),
549                                    createObjectFactory(loader, contextualSupplier, itemType, metas, precomputed),
550                                    new HashMap<>(listEntries), metas, precomputed));
551        }
552
553        // extract nested Object configurations
554        final Map<String, Object> objectEntries = specificMapping.entrySet().stream().filter(e -> {
555            final String key = e.getKey();
556            return key.indexOf('.', prefix.length() + 1) > 0;
557        }).sorted((o1, o2) -> {
558            final String key1 = o1.getKey();
559            final String key2 = o2.getKey();
560            if (key1.equals(key2)) {
561                return 0;
562            }
563
564            final String nestedName1 = key1.substring(prefix.length(), key1.indexOf('.', prefix.length() + 1));
565            final String nestedName2 = key2.substring(prefix.length(), key2.indexOf('.', prefix.length() + 1));
566
567            final int idxStart1 = nestedName1.indexOf('[');
568            final int idxStart2 = nestedName2.indexOf('[');
569            if (idxStart1 > 0 && idxStart2 > 0
570                    && nestedName1.substring(0, idxStart1).equals(nestedName2.substring(0, idxStart2))) {
571                final int idx1 = parseIndex(nestedName1.substring(idxStart1 + 1, nestedName1.length() - 1));
572                final int idx2 = parseIndex(nestedName2.substring(idxStart2 + 1, nestedName2.length() - 1));
573                return idx1 - idx2;
574            }
575            return key1.compareTo(key2);
576        }).collect(toMap(Map.Entry::getKey, Map.Entry::getValue, (o, o2) -> {
577            throw new IllegalArgumentException("Can't merge " + o + " and " + o2);
578        }, LinkedHashMap::new));
579        objectEntries.keySet().forEach(specificMapping::remove);
580        final Map<String, Object> preparedObjects = new HashMap<>();
581        for (final Map.Entry<String, Object> entry : objectEntries.entrySet()) {
582            final String nestedName =
583                    entry.getKey().substring(prefix.length(), entry.getKey().indexOf('.', prefix.length() + 1));
584            if (isUiParam(nestedName)) {
585                continue;
586            }
587            if (nestedName.endsWith("]")) { // complex lists
588                final int idxStart = nestedName.indexOf('[');
589                if (idxStart > 0) {
590                    final String listName = nestedName.substring(0, idxStart);
591                    final Field field = findField(normalizeName(listName, metas), clazz);
592                    if (ParameterizedType.class.isInstance(field.getGenericType())) {
593                        final ParameterizedType pt = ParameterizedType.class.cast(field.getGenericType());
594                        if (Class.class.isInstance(pt.getRawType())) {
595                            final Class<?> rawType = Class.class.cast(pt.getRawType());
596                            if (Set.class.isAssignableFrom(rawType)) {
597                                addListElement(loader, contextualSupplier, config, prefix, preparedObjects, nestedName,
598                                        listName, pt, () -> new HashSet<>(2), translate(metas, listName), precomputed);
599                            } else if (Collection.class.isAssignableFrom(rawType)) {
600                                addListElement(loader, contextualSupplier, config, prefix, preparedObjects, nestedName,
601                                        listName, pt, () -> new ArrayList<>(2), translate(metas, listName),
602                                        precomputed);
603                            } else {
604                                throw new IllegalArgumentException("unsupported configuration type: " + pt);
605                            }
606                            continue;
607                        } else {
608                            throw new IllegalArgumentException("unsupported configuration type: " + pt);
609                        }
610                    } else {
611                        throw new IllegalArgumentException("unsupported configuration type: " + field.getType());
612                    }
613                } else {
614                    throw new IllegalArgumentException("unsupported configuration type: " + nestedName);
615                }
616            }
617            final String fieldName = normalizeName(nestedName, metas);
618            if (preparedObjects.containsKey(fieldName)) {
619                continue;
620            }
621            final Field field = findField(fieldName, clazz);
622            preparedObjects
623                    .put(fieldName,
624                            createObject(loader, contextualSupplier, field.getType(), findArgsName(field.getType()),
625                                    prefix + nestedName, config, translate(metas, nestedName), precomputed));
626        }
627
628        // other entries can be directly set
629        final Map<String, Object> normalizedConfig = specificMapping
630                .entrySet()
631                .stream()
632                .filter(e -> e.getKey().startsWith(prefix) && e.getKey().substring(prefix.length()).indexOf('.') < 0)
633                .collect(toMap(e -> {
634                    final String specificConfig = e.getKey().substring(prefix.length());
635                    final int index = specificConfig.indexOf('[');
636                    if (index > 0) {
637                        final int end = specificConfig.indexOf(']', index);
638                        if (end > index) { // > 0 would work too
639                            // here we need to normalize it to let xbean understand it
640                            String leadingString = specificConfig.substring(0, index);
641                            if (leadingString.endsWith(".key") || leadingString.endsWith(".value")) { // map
642                                leadingString = leadingString.substring(0, leadingString.lastIndexOf('.'));
643                            }
644                            return leadingString + specificConfig.substring(end + 1);
645                        }
646                    }
647                    return specificConfig;
648                }, Map.Entry::getValue));
649
650        // now bind it all to the recipe and builder the instance
651        preparedMaps.forEach(recipe::setFieldProperty);
652        preparedLists.forEach(recipe::setFieldProperty);
653        preparedObjects.forEach(recipe::setFieldProperty);
654        if (!normalizedConfig.isEmpty()) {
655            normalizedConfig
656                    .entrySet()
657                    .stream()
658                    .map(it -> normalize(it, metas))
659                    .forEach(e -> recipe.setFieldProperty(e.getKey(), e.getValue()));
660        }
661        return recipe.create(loader);
662    }
663
664    private ParameterizedType validateCollection(final Class clazz, final String enclosingName,
665            final Type genericType) {
666        if (!ParameterizedType.class.isInstance(genericType)) {
667            throw new IllegalArgumentException(
668                    clazz + "#" + enclosingName + " should be a generic collection and not a " + genericType);
669        }
670        final ParameterizedType pt = ParameterizedType.class.cast(genericType);
671        if (pt.getActualTypeArguments().length != 1 || !Class.class.isInstance(pt.getActualTypeArguments()[0])) {
672            throw new IllegalArgumentException(clazz + "#" + enclosingName
673                    + " should use concrete class items and not a " + pt.getActualTypeArguments()[0]);
674        }
675        return pt;
676    }
677
678    private ParameterizedType validateObject(final Class clazz, final String enclosingName, final Type genericType) {
679        if (!ParameterizedType.class.isInstance(genericType)) {
680            throw new IllegalArgumentException(
681                    clazz + "#" + enclosingName + " should be a generic map and not a " + genericType);
682        }
683        final ParameterizedType pt = ParameterizedType.class.cast(genericType);
684        if (pt.getActualTypeArguments().length != 2 || !Class.class.isInstance(pt.getActualTypeArguments()[0])
685                || !Class.class.isInstance(pt.getActualTypeArguments()[1])) {
686            throw new IllegalArgumentException(clazz + "#" + enclosingName
687                    + " should be a generic map with a key and value class type (" + pt + ")");
688        }
689        return pt;
690    }
691
692    private ObjectRecipe newRecipe(final Class clazz) {
693        final ObjectRecipe recipe = new ObjectRecipe(clazz);
694        recipe.setRegistry(propertyEditorRegistry);
695        recipe.allow(org.apache.xbean.recipe.Option.FIELD_INJECTION);
696        recipe.allow(org.apache.xbean.recipe.Option.PRIVATE_PROPERTIES);
697        recipe.allow(org.apache.xbean.recipe.Option.CASE_INSENSITIVE_PROPERTIES);
698        recipe.allow(org.apache.xbean.recipe.Option.IGNORE_MISSING_PROPERTIES);
699        return recipe;
700    }
701
702    private boolean isUiParam(final String name) {
703        final int dollar = name.indexOf('$');
704        if (dollar >= 0 && name.indexOf("_name", dollar) > dollar) {
705            log.warn("{} is not a valid configuration, it shouldn't be passed to the runtime", name);
706            return true;
707        }
708        return false;
709    }
710
711    private BinaryOperator<Object> noMerge() {
712        return (a, b) -> {
713            throw new IllegalArgumentException("Conflict");
714        };
715    }
716
717    private Map.Entry<String, Object> normalize(final Map.Entry<String, Object> it, final List<ParameterMeta> metas) {
718        return metas == null ? it : metas.stream().filter(m -> m.getName().equals(it.getKey())).findFirst().map(m -> {
719            final String name = findName(m);
720            if (name.equals(it.getKey())) {
721                return it;
722            }
723            return new AbstractMap.SimpleEntry<>(name, it.getValue());
724        }).orElse(it);
725    }
726
727    private String normalizeName(final String name, final List<ParameterMeta> metas) {
728        return metas == null ? name
729                : metas.stream().filter(m -> m.getName().equals(name)).findFirst().map(this::findName).orElse(name);
730    }
731
732    private String findName(final ParameterMeta m) {
733        return ofNullable(m.getSource()).map(ParameterMeta.Source::name).orElse(m.getName());
734    }
735
736    // CHECKSTYLE:OFF
737    private void addListElement(final ClassLoader loader, final Function<Supplier<Object>, Object> contextualSupplier,
738            final Map<String, Object> config, final String prefix, final Map<String, Object> preparedObjects,
739            final String nestedName, final String listName, final ParameterizedType pt, final Supplier<?> init,
740            final List<ParameterMeta> metas, final Map<Class<?>, Object> precomputed) {
741        // CHECKSTYLE:ON
742        final Collection<Object> aggregator =
743                Collection.class.cast(preparedObjects.computeIfAbsent(listName, k -> init.get()));
744        final Class<?> itemType = Class.class.cast(pt.getActualTypeArguments()[0]);
745        final int index = parseIndex(nestedName.substring(listName.length() + 1, nestedName.length() - 1));
746        final int maxSize = getArrayMaxLength(prefix + listName, config);
747        if (aggregator.size() <= index && index < maxSize) {
748            aggregator
749                    .add(createObject(loader, contextualSupplier, itemType, findArgsName(itemType), prefix + nestedName,
750                            config, metas, precomputed));
751        }
752    }
753
754    private Field findField(final String name, final Class clazz) {
755        Class<?> type = clazz;
756        while (type != Object.class && type != null) {
757            try {
758                return type.getDeclaredField(name);
759            } catch (final NoSuchFieldException e) {
760                // no-op
761            }
762            type = type.getSuperclass();
763        }
764        throw new IllegalArgumentException(
765                String.format("Unknown field: %s in class: %s.", name, clazz != null ? clazz.getName() : "null"));
766    }
767
768    private int sortIndexEntry(final Map.Entry<String, Object> e1, final Map.Entry<String, Object> e2) {
769        final String name1 = e1.getKey();
770        final String name2 = e2.getKey();
771        final int index1 = name1.indexOf('[');
772        final int index2 = name2.indexOf('[');
773
774        // same prefix -> sort specifically
775        if (index1 > 0 && index2 == index1 && name1.substring(0, index1).equals(name2.substring(0, index1))) {
776            final int end1 = name1.indexOf(']', index1);
777            final int end2 = name2.indexOf(']', index2);
778            if (end1 > index1 && end2 > index2) {
779                final String idx1 = name1.substring(index1 + 1, end1);
780                final String idx2 = name2.substring(index2 + 1, end2);
781                return parseIndex(idx1) - parseIndex(idx2);
782            } // else not matching so use default sorting
783        }
784        return name1.compareTo(name2);
785    }
786
787    private int parseIndex(final String name) {
788        if ("length".equals(name)) {
789            return -1; // not important, skipped anyway
790        }
791        return Integer.parseInt(name);
792    }
793
794    private Object doConvert(final Class<?> type, final Object value, final Map<Class<?>, Object> precomputed) {
795        if (value == null) { // get the primitive default
796            return getPrimitiveDefault(type);
797        }
798        if (type.isInstance(value)) { // no need of any conversion
799            return value;
800        }
801        if (JsonValue.class.isAssignableFrom(type)) {
802            return createJsonValue(value, precomputed, Json::createReader);
803        }
804        if (propertyEditorRegistry.findConverter(type) != null) { // go through string to convert the value
805            return propertyEditorRegistry.getValue(type, String.valueOf(value));
806        }
807        throw new IllegalArgumentException("Can't convert '" + value + "' to " + type);
808    }
809
810    private Object getPrimitiveDefault(final Class<?> type) {
811        final Type convergedType = Primitives.unwrap(type);
812        if (char.class == convergedType || short.class == convergedType || byte.class == convergedType
813                || int.class == convergedType) {
814            return 0;
815        }
816        if (long.class == convergedType) {
817            return 0L;
818        }
819        if (boolean.class == convergedType) {
820            return false;
821        }
822        if (double.class == convergedType) {
823            return 0.;
824        }
825        if (float.class == convergedType) {
826            return 0f;
827        }
828        return null;
829    }
830
831    public static class JavascriptRegex implements Predicate<CharSequence> {
832
833        private final String regex;
834
835        private final String indicators;
836
837        private JavascriptRegex(final String regex) {
838            if (regex.startsWith("/") && regex.length() > 1) {
839                final int end = regex.lastIndexOf('/');
840                if (end < 0) {
841                    this.regex = regex;
842                    indicators = "";
843                } else {
844                    this.regex = regex.substring(1, end);
845                    indicators = regex.substring(end + 1);
846                }
847            } else {
848                this.regex = regex;
849                indicators = "";
850            }
851        }
852
853        @Override
854        public boolean test(final CharSequence text) {
855            final String script = "new RegExp(regex, indicators).test(text)";
856            final Context context = Context.enter();
857            try {
858                final Scriptable scope = context.initStandardObjects();
859                scope.put("text", scope, text);
860                scope.put("regex", scope, regex);
861                scope.put("indicators", scope, indicators);
862                return Context.toBoolean(context.evaluateString(scope, script, "test", 0, null));
863            } catch (final Exception e) {
864                return false;
865            } finally {
866                Context.exit();
867            }
868        }
869    }
870
871    public interface Messages {
872
873        String required(String property);
874
875        String min(String property, double bound, double value);
876
877        String max(String property, double bound, double value);
878
879        String minLength(String property, double bound, int value);
880
881        String maxLength(String property, double bound, int value);
882
883        String minItems(String property, double bound, int value);
884
885        String maxItems(String property, double bound, int value);
886
887        String uniqueItems(String property);
888
889        String pattern(String property, String pattern);
890    }
891
892    @RequiredArgsConstructor
893    private static class PayloadValidator implements PayloadMapper.OnParameter {
894
895        private static final VisibilityService VISIBILITY_SERVICE = new VisibilityService(JsonProvider.provider());
896
897        private static final Messages MESSAGES = new InternationalizationServiceFactory(Locale::getDefault)
898                .create(Messages.class, PayloadValidator.class.getClassLoader());
899
900        private final boolean skip = Boolean.getBoolean("talend.component.configuration.validation.skip");
901
902        private final Collection<String> errors = new ArrayList<>();
903
904        JsonObject globalPayload;
905
906        @Override
907        public void onParameter(final ParameterMeta meta, final JsonValue value) {
908            if (!VISIBILITY_SERVICE.build(meta).isVisible(globalPayload)) {
909                return;
910            }
911
912            if (Boolean.parseBoolean(meta.getMetadata().get("tcomp::validation::required"))
913                    && value == JsonValue.NULL) {
914                errors.add(MESSAGES.required(meta.getPath()));
915            }
916            final Map<String, String> metadata = meta.getMetadata();
917            {
918                final String min = metadata.get("tcomp::validation::min");
919                if (min != null) {
920                    final double bound = Double.parseDouble(min);
921                    if (value.getValueType() == JsonValue.ValueType.NUMBER
922                            && JsonNumber.class.cast(value).doubleValue() < bound) {
923                        errors.add(MESSAGES.min(meta.getPath(), bound, JsonNumber.class.cast(value).doubleValue()));
924                    }
925                }
926            }
927            {
928                final String max = metadata.get("tcomp::validation::max");
929                if (max != null) {
930                    final double bound = Double.parseDouble(max);
931                    if (value.getValueType() == JsonValue.ValueType.NUMBER
932                            && JsonNumber.class.cast(value).doubleValue() > bound) {
933                        errors.add(MESSAGES.max(meta.getPath(), bound, JsonNumber.class.cast(value).doubleValue()));
934                    }
935                }
936            }
937            {
938                final String min = metadata.get("tcomp::validation::minLength");
939                if (min != null) {
940                    final double bound = Double.parseDouble(min);
941                    if (value.getValueType() == JsonValue.ValueType.STRING) {
942                        final String val = JsonString.class.cast(value).getString();
943                        if (val.length() < bound) {
944                            errors.add(MESSAGES.minLength(meta.getPath(), bound, val.length()));
945                        }
946                    }
947                }
948            }
949            {
950                final String max = metadata.get("tcomp::validation::maxLength");
951                if (max != null) {
952                    final double bound = Double.parseDouble(max);
953                    if (value.getValueType() == JsonValue.ValueType.STRING) {
954                        final String val = JsonString.class.cast(value).getString();
955                        if (val.length() > bound) {
956                            errors.add(MESSAGES.maxLength(meta.getPath(), bound, val.length()));
957                        }
958                    }
959                }
960            }
961            {
962                final String min = metadata.get("tcomp::validation::minItems");
963                if (min != null) {
964                    final double bound = Double.parseDouble(min);
965                    if (value.getValueType() == JsonValue.ValueType.ARRAY && value.asJsonArray().size() < bound) {
966                        errors.add(MESSAGES.minItems(meta.getPath(), bound, value.asJsonArray().size()));
967                    }
968                }
969            }
970            {
971                final String max = metadata.get("tcomp::validation::maxItems");
972                if (max != null) {
973                    final double bound = Double.parseDouble(max);
974                    if (value.getValueType() == JsonValue.ValueType.ARRAY && value.asJsonArray().size() > bound) {
975                        errors.add(MESSAGES.maxItems(meta.getPath(), bound, value.asJsonArray().size()));
976                    }
977                }
978            }
979            {
980                final String unique = metadata.get("tcomp::validation::uniqueItems");
981                if (unique != null) {
982                    if (value.getValueType() == JsonValue.ValueType.ARRAY) {
983                        final JsonArray array = value.asJsonArray();
984                        if (new HashSet<>(array).size() != array.size()) {
985                            errors.add(MESSAGES.uniqueItems(meta.getPath()));
986                        }
987                    }
988                }
989            }
990            {
991                final String pattern = metadata.get("tcomp::validation::pattern");
992                if (pattern != null && value.getValueType() == JsonValue.ValueType.STRING) {
993                    final String val = JsonString.class.cast(value).getString();
994                    if (!new JavascriptRegex(pattern).test(CharSequence.class.cast(val))) {
995                        errors.add(MESSAGES.pattern(meta.getPath(), pattern));
996                    }
997                }
998            }
999        }
1000
1001        private void throwIfFailed() {
1002            if (!errors.isEmpty()) {
1003                throw new IllegalArgumentException("- " + String.join("\n- ", errors));
1004            }
1005        }
1006    }
1007
1008    /**
1009     * Helper function for creating an instance from a configuration map.
1010     * 
1011     * @param clazz Class of the wanted instance.
1012     * @param <T> Type managed
1013     * @return function that generate the wanted instance when calling
1014     * {@link BiFunction#apply(java.lang.Object, java.lang.Object)} with a config name and configuration {@link Map}.
1015     */
1016    public <T> BiFunction<String, Map<String, Object>, T> createObjectFactory(final Class<T> clazz) {
1017        final Map precomputed = Collections.emptyMap();
1018        if (clazz.isPrimitive() || Primitives.unwrap(clazz) != clazz || clazz == String.class) {
1019            return (name, config) -> (T) doConvert(clazz, config.get(name), precomputed);
1020        }
1021        if (clazz.isEnum()) {
1022            return (name,
1023                    config) -> (T) ofNullable(config.get(name))
1024                            .map(String.class::cast)
1025                            .map(String::trim)
1026                            .filter(it -> !it.isEmpty())
1027                            .map(v -> Enum.valueOf((Class<Enum>) clazz, v))
1028                            .orElse(null);
1029        }
1030        final ClassLoader loader = Thread.currentThread().getContextClassLoader();
1031        final Function<Supplier<Object>, Object> contextualSupplier = createContextualSupplier(loader);
1032        final Constructor<?> c = findConstructor(clazz);
1033        final ParameterModelService s = new ParameterModelService(new PropertyEditorRegistry());
1034        final List<ParameterMeta> metas = s.buildParameterMetas(c, c.getDeclaringClass().getPackage().getName(), null);
1035        final String[] args = findArgsName(clazz);
1036        return (name, config) -> (T) contextualSupplier
1037                .apply(() -> createObject(loader, contextualSupplier, clazz, args, name, config, metas, precomputed));
1038    }
1039}