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.server.front;
017
018import static java.util.Collections.singletonList;
019import static java.util.Optional.ofNullable;
020import static java.util.function.Function.identity;
021import static java.util.stream.Collectors.toList;
022import static java.util.stream.Collectors.toMap;
023import static java.util.stream.Collectors.toSet;
024
025import java.util.Collection;
026import java.util.Collections;
027import java.util.HashMap;
028import java.util.Iterator;
029import java.util.Locale;
030import java.util.Map;
031import java.util.concurrent.ConcurrentHashMap;
032import java.util.concurrent.ConcurrentMap;
033import java.util.function.Function;
034import java.util.function.Predicate;
035import java.util.stream.Stream;
036
037import javax.annotation.PostConstruct;
038import javax.cache.annotation.CacheDefaults;
039import javax.cache.annotation.CacheResult;
040import javax.enterprise.context.ApplicationScoped;
041import javax.enterprise.event.Observes;
042import javax.inject.Inject;
043import javax.ws.rs.WebApplicationException;
044import javax.ws.rs.core.Context;
045import javax.ws.rs.core.HttpHeaders;
046import javax.ws.rs.core.Response;
047
048import org.talend.sdk.component.api.exception.ComponentException;
049import org.talend.sdk.component.container.Container;
050import org.talend.sdk.component.design.extension.RepositoryModel;
051import org.talend.sdk.component.design.extension.repository.Config;
052import org.talend.sdk.component.runtime.internationalization.FamilyBundle;
053import org.talend.sdk.component.runtime.manager.ComponentManager;
054import org.talend.sdk.component.server.api.ConfigurationTypeResource;
055import org.talend.sdk.component.server.configuration.ComponentServerConfiguration;
056import org.talend.sdk.component.server.dao.ConfigurationDao;
057import org.talend.sdk.component.server.front.base.internal.RequestKey;
058import org.talend.sdk.component.server.front.model.ConfigTypeNode;
059import org.talend.sdk.component.server.front.model.ConfigTypeNodes;
060import org.talend.sdk.component.server.front.model.ErrorDictionary;
061import org.talend.sdk.component.server.front.model.SimplePropertyDefinition;
062import org.talend.sdk.component.server.front.model.error.ErrorPayload;
063import org.talend.sdk.component.server.front.security.SecurityUtils;
064import org.talend.sdk.component.server.lang.MapCache;
065import org.talend.sdk.component.server.service.ActionsService;
066import org.talend.sdk.component.server.service.ExtensionComponentMetadataManager;
067import org.talend.sdk.component.server.service.LocaleMapper;
068import org.talend.sdk.component.server.service.PropertiesService;
069import org.talend.sdk.component.server.service.SimpleQueryLanguageCompiler;
070import org.talend.sdk.component.server.service.event.DeployedComponent;
071import org.talend.sdk.component.server.service.jcache.FrontCacheKeyGenerator;
072import org.talend.sdk.component.server.service.jcache.FrontCacheResolver;
073
074import lombok.extern.slf4j.Slf4j;
075
076@Slf4j
077@ApplicationScoped
078@CacheDefaults(cacheResolverFactory = FrontCacheResolver.class, cacheKeyGenerator = FrontCacheKeyGenerator.class)
079public class ConfigurationTypeResourceImpl implements ConfigurationTypeResource {
080
081    private final ConcurrentMap<RequestKey, ConfigTypeNodes> indicesPerRequest = new ConcurrentHashMap<>();
082
083    @Inject
084    private ComponentManager manager;
085
086    @Inject
087    private PropertiesService propertiesService;
088
089    @Inject
090    private ActionsService actionsService;
091
092    @Inject
093    private LocaleMapper localeMapper;
094
095    @Inject
096    private ConfigurationDao configurations;
097
098    @Inject
099    private ExtensionComponentMetadataManager virtualComponents;
100
101    @Inject
102    private MapCache caches;
103
104    @Inject
105    private ComponentServerConfiguration configuration;
106
107    @Inject
108    private SimpleQueryLanguageCompiler queryLanguageCompiler;
109
110    @Inject
111    @Context
112    private HttpHeaders headers;
113
114    @Inject
115    private SecurityUtils secUtils;
116
117    private final Map<String, Function<ConfigTypeNode, Object>> configNodeEvaluators = new HashMap<>();
118
119    @PostConstruct
120    private void init() {
121        log.info("Initializing " + getClass());
122        configNodeEvaluators.put("id", ConfigTypeNode::getId);
123        configNodeEvaluators.put("type", ConfigTypeNode::getConfigurationType);
124        configNodeEvaluators.put("name", ConfigTypeNode::getName);
125        configNodeEvaluators.put("metadata", node -> {
126            final Iterator<SimplePropertyDefinition> iterator = node.getProperties().stream().iterator();
127            if (iterator.hasNext()) {
128                return iterator.next().getMetadata();
129            }
130            return Collections.emptyMap();
131        });
132    }
133
134    public void clearCache(@Observes final DeployedComponent deployedComponent) {
135        indicesPerRequest.clear();
136    }
137
138    @Override
139    @CacheResult
140    public ConfigTypeNodes getRepositoryModel(final String language, final boolean lightPayload, final String query) {
141        log.debug("[getRepositoryModel] lang={} lpl={} query={}", language, lightPayload, query);
142        final Locale locale = localeMapper.mapLocale(language);
143        caches.evictIfNeeded(indicesPerRequest, configuration.getMaxCacheSize() - 1);
144        return indicesPerRequest
145                .computeIfAbsent(new RequestKey(locale, !lightPayload, query, null),
146                        key -> toNodes(locale, lightPayload,
147                                it -> true, queryLanguageCompiler.compile(query, configNodeEvaluators)));
148    }
149
150    @Override
151    @CacheResult
152    public ConfigTypeNodes getDetail(final String language, final String[] ids) {
153        final Predicate<String> filter = ids == null ? s -> false : new Predicate<String>() {
154
155            private final Collection<String> values = Stream.of(ids).collect(toSet());
156
157            @Override
158            public boolean test(final String s) {
159                return values.contains(s);
160            }
161        };
162        final Locale locale = localeMapper.mapLocale(language);
163        return toNodes(locale, false, filter, it -> true);
164    }
165
166    @Override
167    public Map<String, String> migrate(final String id, final int version, final Map<String, String> config) {
168        if (virtualComponents.isExtensionEntity(id)) {
169            return config;
170        }
171        final Config configuration = ofNullable(configurations.findById(id))
172                .orElseThrow(() -> new WebApplicationException(Response
173                        .status(Response.Status.NOT_FOUND)
174                        .entity(new ErrorPayload(ErrorDictionary.CONFIGURATION_MISSING,
175                                "Didn't find configuration " + id))
176                        .build()));
177        final Map<String, String> configToMigrate = new HashMap<>(config);
178        final String versionKey = configuration.getMeta().getPath() + ".__version";
179        final boolean addedVersion = configToMigrate.putIfAbsent(versionKey, Integer.toString(version)) == null;
180        try {
181            if (configuration.getVersion() != version) {
182                log.info("[ConfigurationType#migrate] {}#{} registry: {} - incoming: {}.",
183                        configuration.getKey().getFamily(), configuration.getKey().getConfigName(),
184                        configuration.getVersion(), version);
185            }
186            final Map<String, String> migrated = configuration.getMigrationHandler().migrate(version, configToMigrate);
187            if (addedVersion) {
188                migrated.remove(versionKey);
189            }
190            return migrated;
191        } catch (final Exception e) {
192            // contract of migrate() do not impose to throw a ComponentException, so not likely to happen...
193            if (ComponentException.class.isInstance(e)) {
194                final ComponentException ce = (ComponentException) e;
195                throw new WebApplicationException(Response
196                        .status(ce.getErrorOrigin() == ComponentException.ErrorOrigin.USER ? 400
197                                : ce.getErrorOrigin() == ComponentException.ErrorOrigin.BACKEND ? 456 : 520,
198                                "Unexpected migration error")
199                        .entity(new ErrorPayload(ErrorDictionary.UNEXPECTED,
200                                "Migration execution failed with: " + ofNullable(e.getMessage())
201                                        .orElseGet(() -> NullPointerException.class.isInstance(e) ? "unexpected null"
202                                                : "no error message")))
203                        .build());
204            }
205            throw new WebApplicationException(Response
206                    .status(520, "Unexpected migration error")
207                    .entity(new ErrorPayload(ErrorDictionary.UNEXPECTED,
208                            "Migration execution failed with: " + ofNullable(e.getMessage())
209                                    .orElseGet(() -> NullPointerException.class.isInstance(e) ? "unexpected null"
210                                            : "no error message")))
211                    .build());
212        }
213    }
214
215    private Stream<ConfigTypeNode> createNode(final String parentId, final String family, final Stream<Config> configs,
216            final FamilyBundle resourcesBundle, final Container container, final Locale locale,
217            final Predicate<String> idFilter, final boolean lightPayload) {
218        final ClassLoader loader = container.getLoader();
219        if (configs == null) {
220            return Stream.empty();
221        }
222        return configs.flatMap(c -> {
223            final Stream<ConfigTypeNode> configNode;
224            if (idFilter.test(c.getId())) {
225                final ConfigTypeNode node = new ConfigTypeNode();
226                node.setId(c.getId());
227                node.setVersion(c.getVersion());
228                node.setConfigurationType(c.getKey().getConfigType());
229                node.setName(c.getKey().getConfigName());
230                node.setParentId(parentId);
231                node
232                        .setDisplayName(resourcesBundle
233                                .configurationDisplayName(c.getKey().getConfigType(), c.getKey().getConfigName())
234                                .orElse(c.getKey().getConfigName()));
235                if (!lightPayload) {
236                    node.setActions(actionsService.findActions(family, container, locale, c, resourcesBundle));
237
238                    // force configuration as root prefix
239                    final int prefixLen = c.getMeta().getPath().length();
240                    final String forcedPrefix = c.getMeta().getName();
241                    node
242                            .setProperties(propertiesService
243                                    .buildProperties(singletonList(c.getMeta()), loader, locale, null)
244                                    .map(p -> new SimplePropertyDefinition(
245                                            forcedPrefix + p.getPath().substring(prefixLen), p.getName(),
246                                            p.getDisplayName(), p.getType(), p.getDefaultValue(), p.getValidation(),
247                                            p.getMetadata(), p.getPlaceholder(), p.getProposalDisplayNames()))
248                                    .collect(toList()));
249                }
250
251                node.setEdges(c.getChildConfigs().stream().map(Config::getId).collect(toSet()));
252
253                configNode = Stream.of(node);
254            } else {
255                configNode = Stream.empty();
256            }
257
258            return Stream
259                    .concat(configNode, createNode(c.getId(), family, c.getChildConfigs().stream(), resourcesBundle,
260                            container, locale, idFilter, lightPayload));
261        });
262    }
263
264    private ConfigTypeNodes toNodes(final Locale locale, final boolean lightPayload, final Predicate<String> filter,
265            final Predicate<ConfigTypeNode> nodeFilter) {
266        return new ConfigTypeNodes(Stream
267                .concat(getDeployedConfigurations(filter, nodeFilter, lightPayload, locale),
268                        virtualComponents
269                                .getConfigurations()
270                                .stream()
271                                .filter(it -> filter.test(it.getId()))
272                                .filter(nodeFilter)
273                                .map(it -> lightPayload ? copyLight(it) : it))
274                .collect(toMap(ConfigTypeNode::getId, identity())));
275    }
276
277    private ConfigTypeNode copyLight(final ConfigTypeNode it) {
278        return new ConfigTypeNode(it.getId(), it.getVersion(), it.getParentId(), it.getConfigurationType(),
279                it.getName(), it.getDisplayName(), it.getEdges(), null, null);
280    }
281
282    private Stream<ConfigTypeNode> getDeployedConfigurations(final Predicate<String> filter,
283            final Predicate<ConfigTypeNode> nodeFilter, final boolean lightPayload, final Locale locale) {
284        return manager
285                .find(Stream::of)
286                .filter(c -> c.get(RepositoryModel.class) != null)
287                .flatMap(c -> c
288                        .get(RepositoryModel.class)
289                        .getFamilies()
290                        .stream()
291                        .filter(f -> !f.getConfigs().get().isEmpty())
292                        .flatMap(family -> {
293                            final FamilyBundle resourcesBundle = family.getMeta().findBundle(c.getLoader(), locale);
294
295                            final Stream<ConfigTypeNode> familyNode;
296                            if (filter.test(family.getId())) {
297                                final ConfigTypeNode node = new ConfigTypeNode();
298                                node.setId(family.getId());
299                                node.setName(family.getMeta().getName());
300
301                                node.setDisplayName(resourcesBundle.displayName().orElse(family.getMeta().getName()));
302
303                                node.setEdges(family.getConfigs().get().stream().map(Config::getId).collect(toSet()));
304                                familyNode = Stream.of(node);
305                            } else {
306                                familyNode = Stream.empty();
307                            }
308                            return Stream
309                                    .concat(familyNode,
310                                            createNode(family.getId(), family.getMeta().getName(),
311                                                    family.getConfigs().get().stream(), resourcesBundle, c, locale,
312                                                    filter, lightPayload));
313                        }))
314                .filter(nodeFilter);
315    }
316}