001/**
002 * Copyright (C) 2006-2020 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.enterprise.context.ApplicationScoped;
039import javax.enterprise.event.Observes;
040import javax.inject.Inject;
041import javax.ws.rs.WebApplicationException;
042import javax.ws.rs.core.Response;
043
044import org.talend.sdk.component.container.Container;
045import org.talend.sdk.component.design.extension.RepositoryModel;
046import org.talend.sdk.component.design.extension.repository.Config;
047import org.talend.sdk.component.runtime.internationalization.FamilyBundle;
048import org.talend.sdk.component.runtime.manager.ComponentManager;
049import org.talend.sdk.component.server.api.ConfigurationTypeResource;
050import org.talend.sdk.component.server.configuration.ComponentServerConfiguration;
051import org.talend.sdk.component.server.dao.ConfigurationDao;
052import org.talend.sdk.component.server.front.base.internal.RequestKey;
053import org.talend.sdk.component.server.front.model.ConfigTypeNode;
054import org.talend.sdk.component.server.front.model.ConfigTypeNodes;
055import org.talend.sdk.component.server.front.model.ErrorDictionary;
056import org.talend.sdk.component.server.front.model.SimplePropertyDefinition;
057import org.talend.sdk.component.server.front.model.error.ErrorPayload;
058import org.talend.sdk.component.server.lang.MapCache;
059import org.talend.sdk.component.server.service.ActionsService;
060import org.talend.sdk.component.server.service.ExtensionComponentMetadataManager;
061import org.talend.sdk.component.server.service.LocaleMapper;
062import org.talend.sdk.component.server.service.PropertiesService;
063import org.talend.sdk.component.server.service.SimpleQueryLanguageCompiler;
064import org.talend.sdk.component.server.service.event.DeployedComponent;
065
066import lombok.extern.slf4j.Slf4j;
067
068@Slf4j
069@ApplicationScoped
070public class ConfigurationTypeResourceImpl implements ConfigurationTypeResource {
071
072    private final ConcurrentMap<RequestKey, ConfigTypeNodes> indicesPerRequest = new ConcurrentHashMap<>();
073
074    @Inject
075    private ComponentManager manager;
076
077    @Inject
078    private PropertiesService propertiesService;
079
080    @Inject
081    private ActionsService actionsService;
082
083    @Inject
084    private LocaleMapper localeMapper;
085
086    @Inject
087    private ConfigurationDao configurations;
088
089    @Inject
090    private ExtensionComponentMetadataManager virtualComponents;
091
092    @Inject
093    private MapCache caches;
094
095    @Inject
096    private ComponentServerConfiguration configuration;
097
098    @Inject
099    private SimpleQueryLanguageCompiler queryLanguageCompiler;
100
101    private Map<String, Function<ConfigTypeNode, Object>> configNodeEvaluators = new HashMap<>();
102
103    @PostConstruct
104    private void init() {
105        configNodeEvaluators.put("id", ConfigTypeNode::getId);
106        configNodeEvaluators.put("type", ConfigTypeNode::getConfigurationType);
107        configNodeEvaluators.put("name", ConfigTypeNode::getName);
108        configNodeEvaluators.put("metadata", node -> {
109            final Iterator<SimplePropertyDefinition> iterator = node.getProperties().stream().iterator();
110            if (iterator.hasNext()) {
111                return iterator.next().getMetadata();
112            }
113            return Collections.emptyMap();
114        });
115    }
116
117    public void clearCache(@Observes final DeployedComponent deployedComponent) {
118        indicesPerRequest.clear();
119    }
120
121    @Override
122    public ConfigTypeNodes getRepositoryModel(final String language, final boolean lightPayload, final String query) {
123        final Locale locale = localeMapper.mapLocale(language);
124        caches.evictIfNeeded(indicesPerRequest, configuration.getMaxCacheSize() - 1);
125        return indicesPerRequest
126                .computeIfAbsent(new RequestKey(locale, !lightPayload, query), key -> toNodes(locale, lightPayload,
127                        it -> true, queryLanguageCompiler.compile(query, configNodeEvaluators)));
128    }
129
130    @Override
131    public ConfigTypeNodes getDetail(final String language, final String[] ids) {
132        final Predicate<String> filter = ids == null ? s -> false : new Predicate<String>() {
133
134            private final Collection<String> values = Stream.of(ids).collect(toSet());
135
136            @Override
137            public boolean test(final String s) {
138                return values.contains(s);
139            }
140        };
141        final Locale locale = localeMapper.mapLocale(language);
142        return toNodes(locale, false, filter, it -> true);
143    }
144
145    @Override
146    public Map<String, String> migrate(final String id, final int version, final Map<String, String> config) {
147        if (virtualComponents.isExtensionEntity(id)) {
148            return config;
149        }
150        final Config configuration = ofNullable(configurations.findById(id))
151                .orElseThrow(() -> new WebApplicationException(Response
152                        .status(Response.Status.NOT_FOUND)
153                        .entity(new ErrorPayload(ErrorDictionary.CONFIGURATION_MISSING,
154                                "Didn't find configuration " + id))
155                        .build()));
156        final Map<String, String> configToMigrate = new HashMap<>(config);
157        final String versionKey = configuration.getMeta().getPath() + ".__version";
158        final boolean addedVersion = configToMigrate.putIfAbsent(versionKey, Integer.toString(version)) == null;
159        final Map<String, String> migrated = configuration.getMigrationHandler().migrate(version, configToMigrate);
160        if (addedVersion) {
161            migrated.remove(versionKey);
162        }
163        return migrated;
164    }
165
166    private Stream<ConfigTypeNode> createNode(final String parentId, final String family, final Stream<Config> configs,
167            final FamilyBundle resourcesBundle, final Container container, final Locale locale,
168            final Predicate<String> idFilter, final boolean lightPayload) {
169        final ClassLoader loader = container.getLoader();
170        if (configs == null) {
171            return Stream.empty();
172        }
173        return configs.flatMap(c -> {
174            final Stream<ConfigTypeNode> configNode;
175            if (idFilter.test(c.getId())) {
176                final ConfigTypeNode node = new ConfigTypeNode();
177                node.setId(c.getId());
178                node.setVersion(c.getVersion());
179                node.setConfigurationType(c.getKey().getConfigType());
180                node.setName(c.getKey().getConfigName());
181                node.setParentId(parentId);
182                node
183                        .setDisplayName(resourcesBundle
184                                .configurationDisplayName(c.getKey().getConfigType(), c.getKey().getConfigName())
185                                .orElse(c.getKey().getConfigName()));
186                if (!lightPayload) {
187                    node.setActions(actionsService.findActions(family, container, locale, c, resourcesBundle));
188
189                    // force configuration as root prefix
190                    final int prefixLen = c.getMeta().getPath().length();
191                    final String forcedPrefix = c.getMeta().getName();
192                    node
193                            .setProperties(propertiesService
194                                    .buildProperties(singletonList(c.getMeta()), loader, locale, null)
195                                    .map(p -> new SimplePropertyDefinition(
196                                            forcedPrefix + p.getPath().substring(prefixLen), p.getName(),
197                                            p.getDisplayName(), p.getType(), p.getDefaultValue(), p.getValidation(),
198                                            p.getMetadata(), p.getPlaceholder(), p.getProposalDisplayNames()))
199                                    .collect(toList()));
200                }
201
202                node.setEdges(c.getChildConfigs().stream().map(Config::getId).collect(toSet()));
203
204                configNode = Stream.of(node);
205            } else {
206                configNode = Stream.empty();
207            }
208
209            return Stream
210                    .concat(configNode, createNode(c.getId(), family, c.getChildConfigs().stream(), resourcesBundle,
211                            container, locale, idFilter, lightPayload));
212        });
213    }
214
215    private ConfigTypeNodes toNodes(final Locale locale, final boolean lightPayload, final Predicate<String> filter,
216            final Predicate<ConfigTypeNode> nodeFilter) {
217        return new ConfigTypeNodes(Stream
218                .concat(getDeployedConfigurations(filter, nodeFilter, lightPayload, locale),
219                        virtualComponents
220                                .getConfigurations()
221                                .stream()
222                                .filter(it -> filter.test(it.getId()))
223                                .filter(nodeFilter)
224                                .map(it -> lightPayload ? copyLight(it) : it))
225                .collect(toMap(ConfigTypeNode::getId, identity())));
226    }
227
228    private ConfigTypeNode copyLight(final ConfigTypeNode it) {
229        return new ConfigTypeNode(it.getId(), it.getVersion(), it.getParentId(), it.getConfigurationType(),
230                it.getName(), it.getDisplayName(), it.getEdges(), null, null);
231    }
232
233    private Stream<ConfigTypeNode> getDeployedConfigurations(final Predicate<String> filter,
234            final Predicate<ConfigTypeNode> nodeFilter, final boolean lightPayload, final Locale locale) {
235        return manager
236                .find(Stream::of)
237                .filter(c -> c.get(RepositoryModel.class) != null)
238                .flatMap(c -> c
239                        .get(RepositoryModel.class)
240                        .getFamilies()
241                        .stream()
242                        .filter(f -> !f.getConfigs().get().isEmpty())
243                        .flatMap(family -> {
244                            final FamilyBundle resourcesBundle = family.getMeta().findBundle(c.getLoader(), locale);
245
246                            final Stream<ConfigTypeNode> familyNode;
247                            if (filter.test(family.getId())) {
248                                final ConfigTypeNode node = new ConfigTypeNode();
249                                node.setId(family.getId());
250                                node.setName(family.getMeta().getName());
251
252                                node.setDisplayName(resourcesBundle.displayName().orElse(family.getMeta().getName()));
253
254                                node.setEdges(family.getConfigs().get().stream().map(Config::getId).collect(toSet()));
255                                familyNode = Stream.of(node);
256                            } else {
257                                familyNode = Stream.empty();
258                            }
259                            return Stream
260                                    .concat(familyNode,
261                                            createNode(family.getId(), family.getMeta().getName(),
262                                                    family.getConfigs().get().stream(), resourcesBundle, c, locale,
263                                                    filter, lightPayload));
264                        }))
265                .filter(nodeFilter);
266    }
267}