001/**
002 * Copyright (C) 2006-2021 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.lang.MapCache;
064import org.talend.sdk.component.server.service.ActionsService;
065import org.talend.sdk.component.server.service.ExtensionComponentMetadataManager;
066import org.talend.sdk.component.server.service.LocaleMapper;
067import org.talend.sdk.component.server.service.PropertiesService;
068import org.talend.sdk.component.server.service.SimpleQueryLanguageCompiler;
069import org.talend.sdk.component.server.service.event.DeployedComponent;
070import org.talend.sdk.component.server.service.jcache.FrontCacheKeyGenerator;
071import org.talend.sdk.component.server.service.jcache.FrontCacheResolver;
072import org.talend.sdk.components.vault.client.VaultClient;
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 VaultClient vault;
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        final Locale locale = localeMapper.mapLocale(language);
142        caches.evictIfNeeded(indicesPerRequest, configuration.getMaxCacheSize() - 1);
143        return indicesPerRequest
144                .computeIfAbsent(new RequestKey(locale, !lightPayload, query), key -> toNodes(locale, lightPayload,
145                        it -> true, queryLanguageCompiler.compile(query, configNodeEvaluators)));
146    }
147
148    @Override
149    @CacheResult
150    public ConfigTypeNodes getDetail(final String language, final String[] ids) {
151        final Predicate<String> filter = ids == null ? s -> false : new Predicate<String>() {
152
153            private final Collection<String> values = Stream.of(ids).collect(toSet());
154
155            @Override
156            public boolean test(final String s) {
157                return values.contains(s);
158            }
159        };
160        final Locale locale = localeMapper.mapLocale(language);
161        return toNodes(locale, false, filter, it -> true);
162    }
163
164    @Override
165    public Map<String, String> migrate(final String id, final int version, final Map<String, String> config) {
166        String tenant;
167        try {
168            tenant = headers.getHeaderString("x-talend-tenant-id");
169        } catch (Exception e) {
170            log.debug("[migrate] context not applicable: {}", e.getMessage());
171            tenant = null;
172        }
173        final Map<String, String> decrypted = vault.decrypt(config, tenant);
174        if (virtualComponents.isExtensionEntity(id)) {
175            return decrypted;
176        }
177        final Config configuration = ofNullable(configurations.findById(id))
178                .orElseThrow(() -> new WebApplicationException(Response
179                        .status(Response.Status.NOT_FOUND)
180                        .entity(new ErrorPayload(ErrorDictionary.CONFIGURATION_MISSING,
181                                "Didn't find configuration " + id))
182                        .build()));
183        final Map<String, String> configToMigrate = new HashMap<>(decrypted);
184        final String versionKey = configuration.getMeta().getPath() + ".__version";
185        final boolean addedVersion = configToMigrate.putIfAbsent(versionKey, Integer.toString(version)) == null;
186        try {
187            final Map<String, String> migrated = configuration.getMigrationHandler().migrate(version, configToMigrate);
188            if (addedVersion) {
189                migrated.remove(versionKey);
190            }
191            return migrated;
192        } catch (final Exception e) {
193            // contract of migrate() do not impose to throw a ComponentException, so not likely to happen...
194            if (ComponentException.class.isInstance(e)) {
195                final ComponentException ce = (ComponentException) e;
196                throw new WebApplicationException(Response
197                        .status(ce.getErrorOrigin() == ComponentException.ErrorOrigin.USER ? 400
198                                : ce.getErrorOrigin() == ComponentException.ErrorOrigin.BACKEND ? 456 : 520,
199                                "Unexpected migration error")
200                        .entity(new ErrorPayload(ErrorDictionary.UNEXPECTED,
201                                "Migration execution failed with: " + ofNullable(e.getMessage())
202                                        .orElseGet(() -> NullPointerException.class.isInstance(e) ? "unexpected null"
203                                                : "no error message")))
204                        .build());
205            }
206            throw new WebApplicationException(Response
207                    .status(520, "Unexpected migration error")
208                    .entity(new ErrorPayload(ErrorDictionary.UNEXPECTED,
209                            "Migration execution failed with: " + ofNullable(e.getMessage())
210                                    .orElseGet(() -> NullPointerException.class.isInstance(e) ? "unexpected null"
211                                            : "no error message")))
212                    .build());
213        }
214    }
215
216    private Stream<ConfigTypeNode> createNode(final String parentId, final String family, final Stream<Config> configs,
217            final FamilyBundle resourcesBundle, final Container container, final Locale locale,
218            final Predicate<String> idFilter, final boolean lightPayload) {
219        final ClassLoader loader = container.getLoader();
220        if (configs == null) {
221            return Stream.empty();
222        }
223        return configs.flatMap(c -> {
224            final Stream<ConfigTypeNode> configNode;
225            if (idFilter.test(c.getId())) {
226                final ConfigTypeNode node = new ConfigTypeNode();
227                node.setId(c.getId());
228                node.setVersion(c.getVersion());
229                node.setConfigurationType(c.getKey().getConfigType());
230                node.setName(c.getKey().getConfigName());
231                node.setParentId(parentId);
232                node
233                        .setDisplayName(resourcesBundle
234                                .configurationDisplayName(c.getKey().getConfigType(), c.getKey().getConfigName())
235                                .orElse(c.getKey().getConfigName()));
236                if (!lightPayload) {
237                    node.setActions(actionsService.findActions(family, container, locale, c, resourcesBundle));
238
239                    // force configuration as root prefix
240                    final int prefixLen = c.getMeta().getPath().length();
241                    final String forcedPrefix = c.getMeta().getName();
242                    node
243                            .setProperties(propertiesService
244                                    .buildProperties(singletonList(c.getMeta()), loader, locale, null)
245                                    .map(p -> new SimplePropertyDefinition(
246                                            forcedPrefix + p.getPath().substring(prefixLen), p.getName(),
247                                            p.getDisplayName(), p.getType(), p.getDefaultValue(), p.getValidation(),
248                                            p.getMetadata(), p.getPlaceholder(), p.getProposalDisplayNames()))
249                                    .collect(toList()));
250                }
251
252                node.setEdges(c.getChildConfigs().stream().map(Config::getId).collect(toSet()));
253
254                configNode = Stream.of(node);
255            } else {
256                configNode = Stream.empty();
257            }
258
259            return Stream
260                    .concat(configNode, createNode(c.getId(), family, c.getChildConfigs().stream(), resourcesBundle,
261                            container, locale, idFilter, lightPayload));
262        });
263    }
264
265    private ConfigTypeNodes toNodes(final Locale locale, final boolean lightPayload, final Predicate<String> filter,
266            final Predicate<ConfigTypeNode> nodeFilter) {
267        return new ConfigTypeNodes(Stream
268                .concat(getDeployedConfigurations(filter, nodeFilter, lightPayload, locale),
269                        virtualComponents
270                                .getConfigurations()
271                                .stream()
272                                .filter(it -> filter.test(it.getId()))
273                                .filter(nodeFilter)
274                                .map(it -> lightPayload ? copyLight(it) : it))
275                .collect(toMap(ConfigTypeNode::getId, identity())));
276    }
277
278    private ConfigTypeNode copyLight(final ConfigTypeNode it) {
279        return new ConfigTypeNode(it.getId(), it.getVersion(), it.getParentId(), it.getConfigurationType(),
280                it.getName(), it.getDisplayName(), it.getEdges(), null, null);
281    }
282
283    private Stream<ConfigTypeNode> getDeployedConfigurations(final Predicate<String> filter,
284            final Predicate<ConfigTypeNode> nodeFilter, final boolean lightPayload, final Locale locale) {
285        return manager
286                .find(Stream::of)
287                .filter(c -> c.get(RepositoryModel.class) != null)
288                .flatMap(c -> c
289                        .get(RepositoryModel.class)
290                        .getFamilies()
291                        .stream()
292                        .filter(f -> !f.getConfigs().get().isEmpty())
293                        .flatMap(family -> {
294                            final FamilyBundle resourcesBundle = family.getMeta().findBundle(c.getLoader(), locale);
295
296                            final Stream<ConfigTypeNode> familyNode;
297                            if (filter.test(family.getId())) {
298                                final ConfigTypeNode node = new ConfigTypeNode();
299                                node.setId(family.getId());
300                                node.setName(family.getMeta().getName());
301
302                                node.setDisplayName(resourcesBundle.displayName().orElse(family.getMeta().getName()));
303
304                                node.setEdges(family.getConfigs().get().stream().map(Config::getId).collect(toSet()));
305                                familyNode = Stream.of(node);
306                            } else {
307                                familyNode = Stream.empty();
308                            }
309                            return Stream
310                                    .concat(familyNode,
311                                            createNode(family.getId(), family.getMeta().getName(),
312                                                    family.getConfigs().get().stream(), resourcesBundle, c, locale,
313                                                    filter, lightPayload));
314                        }))
315                .filter(nodeFilter);
316    }
317}