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}