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}