001/** 002 * Copyright (C) 2006-2022 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 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 174 if (virtualComponents.isExtensionEntity(id)) { 175 return config; 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> decrypted = secUtils.decrypt(singletonList(configuration.getMeta()), config, tenant); 184 185 final Map<String, String> configToMigrate = new HashMap<>(decrypted); 186 final String versionKey = configuration.getMeta().getPath() + ".__version"; 187 final boolean addedVersion = configToMigrate.putIfAbsent(versionKey, Integer.toString(version)) == null; 188 try { 189 final Map<String, String> migrated = configuration.getMigrationHandler().migrate(version, configToMigrate); 190 if (addedVersion) { 191 migrated.remove(versionKey); 192 } 193 return migrated; 194 } catch (final Exception e) { 195 // contract of migrate() do not impose to throw a ComponentException, so not likely to happen... 196 if (ComponentException.class.isInstance(e)) { 197 final ComponentException ce = (ComponentException) e; 198 throw new WebApplicationException(Response 199 .status(ce.getErrorOrigin() == ComponentException.ErrorOrigin.USER ? 400 200 : ce.getErrorOrigin() == ComponentException.ErrorOrigin.BACKEND ? 456 : 520, 201 "Unexpected migration error") 202 .entity(new ErrorPayload(ErrorDictionary.UNEXPECTED, 203 "Migration execution failed with: " + ofNullable(e.getMessage()) 204 .orElseGet(() -> NullPointerException.class.isInstance(e) ? "unexpected null" 205 : "no error message"))) 206 .build()); 207 } 208 throw new WebApplicationException(Response 209 .status(520, "Unexpected migration error") 210 .entity(new ErrorPayload(ErrorDictionary.UNEXPECTED, 211 "Migration execution failed with: " + ofNullable(e.getMessage()) 212 .orElseGet(() -> NullPointerException.class.isInstance(e) ? "unexpected null" 213 : "no error message"))) 214 .build()); 215 } 216 } 217 218 private Stream<ConfigTypeNode> createNode(final String parentId, final String family, final Stream<Config> configs, 219 final FamilyBundle resourcesBundle, final Container container, final Locale locale, 220 final Predicate<String> idFilter, final boolean lightPayload) { 221 final ClassLoader loader = container.getLoader(); 222 if (configs == null) { 223 return Stream.empty(); 224 } 225 return configs.flatMap(c -> { 226 final Stream<ConfigTypeNode> configNode; 227 if (idFilter.test(c.getId())) { 228 final ConfigTypeNode node = new ConfigTypeNode(); 229 node.setId(c.getId()); 230 node.setVersion(c.getVersion()); 231 node.setConfigurationType(c.getKey().getConfigType()); 232 node.setName(c.getKey().getConfigName()); 233 node.setParentId(parentId); 234 node 235 .setDisplayName(resourcesBundle 236 .configurationDisplayName(c.getKey().getConfigType(), c.getKey().getConfigName()) 237 .orElse(c.getKey().getConfigName())); 238 if (!lightPayload) { 239 node.setActions(actionsService.findActions(family, container, locale, c, resourcesBundle)); 240 241 // force configuration as root prefix 242 final int prefixLen = c.getMeta().getPath().length(); 243 final String forcedPrefix = c.getMeta().getName(); 244 node 245 .setProperties(propertiesService 246 .buildProperties(singletonList(c.getMeta()), loader, locale, null) 247 .map(p -> new SimplePropertyDefinition( 248 forcedPrefix + p.getPath().substring(prefixLen), p.getName(), 249 p.getDisplayName(), p.getType(), p.getDefaultValue(), p.getValidation(), 250 p.getMetadata(), p.getPlaceholder(), p.getProposalDisplayNames())) 251 .collect(toList())); 252 } 253 254 node.setEdges(c.getChildConfigs().stream().map(Config::getId).collect(toSet())); 255 256 configNode = Stream.of(node); 257 } else { 258 configNode = Stream.empty(); 259 } 260 261 return Stream 262 .concat(configNode, createNode(c.getId(), family, c.getChildConfigs().stream(), resourcesBundle, 263 container, locale, idFilter, lightPayload)); 264 }); 265 } 266 267 private ConfigTypeNodes toNodes(final Locale locale, final boolean lightPayload, final Predicate<String> filter, 268 final Predicate<ConfigTypeNode> nodeFilter) { 269 return new ConfigTypeNodes(Stream 270 .concat(getDeployedConfigurations(filter, nodeFilter, lightPayload, locale), 271 virtualComponents 272 .getConfigurations() 273 .stream() 274 .filter(it -> filter.test(it.getId())) 275 .filter(nodeFilter) 276 .map(it -> lightPayload ? copyLight(it) : it)) 277 .collect(toMap(ConfigTypeNode::getId, identity()))); 278 } 279 280 private ConfigTypeNode copyLight(final ConfigTypeNode it) { 281 return new ConfigTypeNode(it.getId(), it.getVersion(), it.getParentId(), it.getConfigurationType(), 282 it.getName(), it.getDisplayName(), it.getEdges(), null, null); 283 } 284 285 private Stream<ConfigTypeNode> getDeployedConfigurations(final Predicate<String> filter, 286 final Predicate<ConfigTypeNode> nodeFilter, final boolean lightPayload, final Locale locale) { 287 return manager 288 .find(Stream::of) 289 .filter(c -> c.get(RepositoryModel.class) != null) 290 .flatMap(c -> c 291 .get(RepositoryModel.class) 292 .getFamilies() 293 .stream() 294 .filter(f -> !f.getConfigs().get().isEmpty()) 295 .flatMap(family -> { 296 final FamilyBundle resourcesBundle = family.getMeta().findBundle(c.getLoader(), locale); 297 298 final Stream<ConfigTypeNode> familyNode; 299 if (filter.test(family.getId())) { 300 final ConfigTypeNode node = new ConfigTypeNode(); 301 node.setId(family.getId()); 302 node.setName(family.getMeta().getName()); 303 304 node.setDisplayName(resourcesBundle.displayName().orElse(family.getMeta().getName())); 305 306 node.setEdges(family.getConfigs().get().stream().map(Config::getId).collect(toSet())); 307 familyNode = Stream.of(node); 308 } else { 309 familyNode = Stream.empty(); 310 } 311 return Stream 312 .concat(familyNode, 313 createNode(family.getId(), family.getMeta().getName(), 314 family.getConfigs().get().stream(), resourcesBundle, c, locale, 315 filter, lightPayload)); 316 })) 317 .filter(nodeFilter); 318 } 319}