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}