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}