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.reflect; 017 018import static java.util.Collections.emptyMap; 019import static java.util.Optional.ofNullable; 020import static java.util.stream.Collectors.toMap; 021import static java.util.stream.Stream.concat; 022import static org.talend.sdk.component.runtime.base.lang.exception.InvocationExceptionWrapper.toRuntimeException; 023import static org.talend.sdk.component.runtime.manager.util.Lazy.lazy; 024 025import java.lang.reflect.InvocationTargetException; 026import java.util.Collection; 027import java.util.Collections; 028import java.util.Comparator; 029import java.util.HashMap; 030import java.util.List; 031import java.util.Map; 032import java.util.Objects; 033import java.util.function.Function; 034import java.util.function.Supplier; 035import java.util.stream.Stream; 036 037import org.talend.sdk.component.api.component.MigrationHandler; 038import org.talend.sdk.component.api.component.Version; 039import org.talend.sdk.component.runtime.manager.ComponentManager; 040import org.talend.sdk.component.runtime.manager.ParameterMeta; 041 042import lombok.AllArgsConstructor; 043import lombok.extern.slf4j.Slf4j; 044 045@Slf4j 046@AllArgsConstructor 047public class MigrationHandlerFactory { 048 049 private static final MigrationHandler NO_MIGRATION = (incomingVersion, incomingData) -> incomingData; 050 051 private final ReflectionService reflections; 052 053 public MigrationHandler findMigrationHandler(final Supplier<List<ParameterMeta>> parameterMetas, 054 final Class<?> type, final ComponentManager.AllServices services) { 055 return (incomingVersion, incomingData) -> lazy(() -> createHandler(parameterMetas.get(), type, services)) 056 .get() 057 .migrate(incomingVersion, incomingData); 058 } 059 060 private MigrationHandler createHandler(final List<ParameterMeta> parameterMetas, final Class<?> type, 061 final ComponentManager.AllServices services) { 062 final MigrationHandler implicitMigrationHandler = ofNullable(parameterMetas) 063 .map(Collection::stream) 064 .orElseGet(Stream::empty) 065 .flatMap(this::getNestedConfigType) 066 .sorted(Comparator.comparingInt(o -> o.getPath().length())) 067 .map(p -> { 068 // for now we can assume it is not in arrays 069 final Class<?> jType = Class.class.cast(p.getJavaType()); 070 final MigrationHandler handler = findMigrationHandler(Collections::emptyList, jType, services); 071 if (handler == NO_MIGRATION) { 072 return null; 073 } 074 075 return (Function<Map<String, String>, Map<String, String>>) map -> buildMigrationFunction(p, 076 handler, p.getPath(), map, 077 ofNullable(jType.getAnnotation(Version.class)).map(Version::value).orElse(-1)); 078 }) 079 .filter(Objects::nonNull) 080 .reduce(NO_MIGRATION, 081 (current, 082 partial) -> (incomingVersion, incomingData) -> current 083 .migrate(incomingVersion, partial.apply(incomingData)), 084 (h1, h2) -> (incomingVersion, incomingData) -> h2 085 .migrate(incomingVersion, h1.migrate(incomingVersion, incomingData))); 086 087 if (parameterMetas != null && parameterMetas.size() == 1 088 && parameterMetas.iterator().next().getJavaType() == type) { 089 return implicitMigrationHandler; 090 } 091 return ofNullable(type.getAnnotation(Version.class)) 092 .map(Version::migrationHandler) 093 .filter(t -> t != MigrationHandler.class) 094 .flatMap(t -> Stream 095 .of(t.getConstructors()) 096 .min((o1, o2) -> o2.getParameterCount() - o1.getParameterCount())) 097 .map(t -> services.getServices().computeIfAbsent(t.getDeclaringClass(), k -> { 098 try { 099 return t 100 .newInstance(reflections 101 .parameterFactory(t, services.getServices(), null) 102 .apply(emptyMap())); 103 } catch (final InstantiationException | IllegalAccessException e) { 104 throw new IllegalArgumentException(e); 105 } catch (final InvocationTargetException e) { 106 throw toRuntimeException(e); 107 } 108 })) 109 .map(MigrationHandler.class::cast) 110 .map(h -> { 111 if (implicitMigrationHandler == NO_MIGRATION) { 112 return h; 113 } 114 return (MigrationHandler) (incomingVersion, incomingData) -> { 115 final Map<String, String> configuration = 116 implicitMigrationHandler.migrate(incomingVersion, incomingData); 117 return h.migrate(incomingVersion, configuration); 118 }; 119 }) 120 .orElse(implicitMigrationHandler); 121 } 122 123 private Stream<ParameterMeta> getNestedConfigType(final ParameterMeta parameterMeta) { 124 if (parameterMeta.getNestedParameters().isEmpty() && parameterMeta.getType() != ParameterMeta.Type.OBJECT) { 125 return Stream.empty(); 126 } 127 return concat( 128 (parameterMeta.getJavaType() instanceof Class && parameterMeta 129 .getMetadata() 130 .keySet() 131 .stream() 132 .anyMatch(k -> k.startsWith("tcomp::configurationtype::"))) ? Stream.of(parameterMeta) 133 : Stream.empty(), 134 ofNullable(parameterMeta.getNestedParameters()) 135 .map(Collection::stream) 136 .orElseGet(Stream::empty) 137 .flatMap(this::getNestedConfigType)); 138 } 139 140 private Map<String, String> buildMigrationFunction(final ParameterMeta p, final MigrationHandler handler, 141 final String prefix, final Map<String, String> map, final Integer currentVersion) { 142 final String versionPath = String.format("%s.__version", prefix); 143 final String version = map.get(versionPath); 144 final Map<String, String> result = new HashMap<>(map); 145 if (version != null && Integer.parseInt(version.trim()) < currentVersion) { 146 final Map<String, String> toMigrate = stripPrefix(prefix, map); 147 toMigrate.keySet().forEach(result::remove); 148 final Map<String, String> migrated = ofNullable(handler 149 .migrate(Integer.parseInt(version.trim()), toMigrate 150 .entrySet() 151 .stream() 152 .collect(toMap(e -> e.getKey().substring(prefix.length() + 1), Map.Entry::getValue)))) 153 .orElseGet(Collections::emptyMap); 154 result 155 .putAll(migrated 156 .entrySet() 157 .stream() 158 .collect(toMap(e -> prefix + '.' + e.getKey(), Map.Entry::getValue))); 159 result.put(versionPath, currentVersion.toString()); 160 } else { 161 log.debug("No version for {} so skipping any potential migration", p.getJavaType().toString()); 162 } 163 return result; 164 } 165 166 private Map<String, String> stripPrefix(final String prefix, final Map<String, String> map) { 167 return map 168 .entrySet() 169 .stream() 170 .filter(e -> e.getKey().startsWith(prefix + '.') && !e.getKey().endsWith(".__version")) 171 .collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); 172 } 173}