001/**
002 * Copyright (C) 2006-2025 Talend Inc. - www.talend.com
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.talend.sdk.component.runtime.manager.configuration;
017
018import static java.util.Arrays.asList;
019import static java.util.Collections.emptyMap;
020import static java.util.Collections.singletonMap;
021import static java.util.Optional.ofNullable;
022import static java.util.stream.Collectors.toList;
023import static java.util.stream.Collectors.toMap;
024
025import java.lang.reflect.Field;
026import java.util.AbstractMap;
027import java.util.Collection;
028import java.util.Collections;
029import java.util.HashMap;
030import java.util.List;
031import java.util.Map;
032import java.util.Optional;
033import java.util.concurrent.atomic.AtomicInteger;
034import java.util.stream.Stream;
035
036import org.talend.sdk.component.api.configuration.Option;
037import org.talend.sdk.component.runtime.manager.ParameterMeta;
038
039public class ConfigurationMapper {
040
041    public Map<String, String> map(final List<ParameterMeta> nestedParameters, final Object instance) {
042        return map(nestedParameters, instance, new HashMap<>());
043    }
044
045    private Map<String, String> map(final List<ParameterMeta> nestedParameters, final Object instance,
046            final Map<Integer, Integer> indexes) {
047        if (nestedParameters == null) {
048            return emptyMap();
049        }
050        return nestedParameters.stream().map(param -> {
051            final Object value = getValue(instance, param.getName());
052            if (value == null) {
053                return Collections.<String, String> emptyMap();
054            }
055
056            switch (param.getType()) {
057                case OBJECT:
058                    return map(param.getNestedParameters(), value, indexes);
059                case ARRAY:
060                    final Collection<Object> values = Collection.class.isInstance(value) ? Collection.class.cast(value)
061                            : /* array */asList(Object[].class.cast(value));
062                    final int arrayIndex = indexes.keySet().size();
063                    final AtomicInteger valuesIndex = new AtomicInteger(0);
064                    final Map<String, String> config = values.stream()
065                            .map((Object item) -> {
066                                indexes.put(arrayIndex, valuesIndex.getAndIncrement());
067                                final Map<String, String> res = param
068                                        .getNestedParameters()
069                                        .stream()
070                                        .filter(ConfigurationMapper::isPrimitive)
071                                        .map(p -> new AbstractMap.SimpleImmutableEntry<>(
072                                                evaluateIndexes(p.getPath(), indexes),
073                                                getValue(item, p.getName())))
074                                        .filter(p -> p.getValue() != null)
075                                        .collect(toMap(AbstractMap.SimpleImmutableEntry::getKey,
076                                                p -> String.valueOf(p.getValue())));
077
078                                res.putAll(map(
079                                        param.getNestedParameters()
080                                                .stream()
081                                                .filter(p -> !isPrimitive(p))
082                                                .collect(toList()),
083                                        item, indexes));
084                                return res;
085                            })
086                            .flatMap(m -> m.entrySet().stream())
087                            .collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
088
089                    indexes.remove(arrayIndex); // clear index after the end of array handling
090                    return config;
091
092                default: // primitives
093                    return singletonMap(evaluateIndexes(param.getPath(), indexes), value.toString());
094            }
095        }).flatMap(m -> m.entrySet().stream()).collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
096    }
097
098    private static boolean isPrimitive(final ParameterMeta next) {
099        return Stream
100                .of(ParameterMeta.Type.STRING, ParameterMeta.Type.BOOLEAN, ParameterMeta.Type.ENUM,
101                        ParameterMeta.Type.NUMBER)
102                .anyMatch(v -> v == next.getType());
103    }
104
105    private static Object getValue(final Object instance, final String name) {
106        if (name.endsWith("[${index}]")) {
107            return instance;
108        }
109
110        Field declaredField = null;
111        Class<?> current = instance.getClass();
112        while (current != null && current != Object.class) {
113            final Optional<Field> field = Stream
114                    .of(current.getDeclaredFields())
115                    .filter(it -> ofNullable(it.getAnnotation(Option.class))
116                            .map(Option::value)
117                            .filter(val -> !val.isEmpty())
118                            .orElseGet(it::getName)
119                            .equals(name))
120                    .findFirst();
121            if (!field.isPresent()) {
122                current = current.getSuperclass();
123                continue;
124            }
125            declaredField = field.get();
126            break;
127        }
128        if (declaredField == null) {
129            throw new IllegalArgumentException("No field '" + name + "' in " + instance);
130        }
131        if (!declaredField.isAccessible()) {
132            declaredField.setAccessible(true);
133        }
134        try {
135            return declaredField.get(instance);
136        } catch (final IllegalAccessException e) {
137            throw new IllegalStateException(e);
138        }
139    }
140
141    private static String evaluateIndexes(final String path, final Map<Integer, Integer> indexes) {
142        if (indexes == null || indexes.isEmpty()) {
143            return path;
144        }
145        final String placeholder = "${index}";
146        String p = path;
147        StringBuilder evaluatedPath = new StringBuilder();
148        for (Map.Entry<Integer, Integer> index : indexes.entrySet()) {
149            int i = p.indexOf(placeholder);
150            evaluatedPath.append(p, 0, i).append(index.getValue()).append("]");
151            p = p.substring(i + placeholder.length() + 1);
152        }
153        return evaluatedPath.append(p).toString();
154    }
155}