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}