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.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().map((Object item) -> {
065                    indexes.put(arrayIndex, valuesIndex.getAndIncrement());
066                    final Map<String, String> res = param
067                            .getNestedParameters()
068                            .stream()
069                            .filter(ConfigurationMapper::isPrimitive)
070                            .map(p -> new AbstractMap.SimpleImmutableEntry<>(evaluateIndexes(p.getPath(), indexes),
071                                    getValue(item, p.getName())))
072                            .filter(p -> p.getValue() != null)
073                            .collect(
074                                    toMap(AbstractMap.SimpleImmutableEntry::getKey, p -> String.valueOf(p.getValue())));
075
076                    res
077                            .putAll(map(
078                                    param.getNestedParameters().stream().filter(p -> !isPrimitive(p)).collect(toList()),
079                                    item, indexes));
080                    return res;
081                }).flatMap(m -> m.entrySet().stream()).collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
082
083                indexes.remove(arrayIndex); // clear index after the end of array handling
084                return config;
085
086            default: // primitives
087                return singletonMap(evaluateIndexes(param.getPath(), indexes), value.toString());
088            }
089        }).flatMap(m -> m.entrySet().stream()).collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
090    }
091
092    private static boolean isPrimitive(final ParameterMeta next) {
093        return Stream
094                .of(ParameterMeta.Type.STRING, ParameterMeta.Type.BOOLEAN, ParameterMeta.Type.ENUM,
095                        ParameterMeta.Type.NUMBER)
096                .anyMatch(v -> v == next.getType());
097    }
098
099    private static Object getValue(final Object instance, final String name) {
100        if (name.endsWith("[${index}]")) {
101            return instance;
102        }
103
104        Field declaredField = null;
105        Class<?> current = instance.getClass();
106        while (current != null && current != Object.class) {
107            final Optional<Field> field = Stream
108                    .of(current.getDeclaredFields())
109                    .filter(it -> ofNullable(it.getAnnotation(Option.class))
110                            .map(Option::value)
111                            .filter(val -> !val.isEmpty())
112                            .orElseGet(it::getName)
113                            .equals(name))
114                    .findFirst();
115            if (!field.isPresent()) {
116                current = current.getSuperclass();
117                continue;
118            }
119            declaredField = field.get();
120            break;
121        }
122        if (declaredField == null) {
123            throw new IllegalArgumentException("No field '" + name + "' in " + instance);
124        }
125        if (!declaredField.isAccessible()) {
126            declaredField.setAccessible(true);
127        }
128        try {
129            return declaredField.get(instance);
130        } catch (final IllegalAccessException e) {
131            throw new IllegalStateException(e);
132        }
133    }
134
135    private static String evaluateIndexes(final String path, final Map<Integer, Integer> indexes) {
136        if (indexes == null || indexes.isEmpty()) {
137            return path;
138        }
139        final String placeholder = "${index}";
140        String p = path;
141        StringBuilder evaluatedPath = new StringBuilder();
142        for (Map.Entry<Integer, Integer> index : indexes.entrySet()) {
143            int i = p.indexOf(placeholder);
144            evaluatedPath.append(p, 0, i).append(index.getValue()).append("]");
145            p = p.substring(i + placeholder.length() + 1);
146        }
147        return evaluatedPath.append(p).toString();
148    }
149}