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.emptyList; 019import static java.util.Collections.emptyMap; 020import static java.util.Collections.singletonList; 021import static java.util.Optional.ofNullable; 022import static java.util.function.UnaryOperator.identity; 023import static java.util.stream.Collectors.toList; 024import static java.util.stream.Collectors.toMap; 025import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE; 026import static javax.xml.transform.OutputKeys.OMIT_XML_DECLARATION; 027import static org.talend.sdk.component.server.front.model.ErrorDictionary.COMPONENT_MISSING; 028import static org.talend.sdk.component.server.front.model.ErrorDictionary.DESIGN_MODEL_MISSING; 029import static org.talend.sdk.component.server.front.model.ErrorDictionary.PLUGIN_MISSING; 030 031import java.io.BufferedInputStream; 032import java.io.IOException; 033import java.io.InputStream; 034import java.io.StringWriter; 035import java.io.Writer; 036import java.nio.charset.StandardCharsets; 037import java.nio.file.Files; 038import java.nio.file.Path; 039import java.util.AbstractMap.SimpleEntry; 040import java.util.Base64; 041import java.util.Collections; 042import java.util.HashMap; 043import java.util.Iterator; 044import java.util.List; 045import java.util.Locale; 046import java.util.Map; 047import java.util.Map.Entry; 048import java.util.Objects; 049import java.util.Optional; 050import java.util.concurrent.ConcurrentHashMap; 051import java.util.concurrent.ConcurrentMap; 052import java.util.function.Function; 053import java.util.function.Predicate; 054import java.util.function.Supplier; 055import java.util.stream.Stream; 056 057import javax.annotation.PostConstruct; 058import javax.cache.annotation.CacheDefaults; 059import javax.cache.annotation.CacheResult; 060import javax.enterprise.context.ApplicationScoped; 061import javax.enterprise.event.Observes; 062import javax.inject.Inject; 063import javax.ws.rs.WebApplicationException; 064import javax.ws.rs.core.Context; 065import javax.ws.rs.core.HttpHeaders; 066import javax.ws.rs.core.MediaType; 067import javax.ws.rs.core.Response; 068import javax.ws.rs.core.Response.Status; 069import javax.ws.rs.core.StreamingOutput; 070import javax.xml.XMLConstants; 071import javax.xml.parsers.DocumentBuilderFactory; 072import javax.xml.transform.Transformer; 073import javax.xml.transform.TransformerFactory; 074import javax.xml.transform.dom.DOMSource; 075import javax.xml.transform.stream.StreamResult; 076 077import org.talend.sdk.component.container.Container; 078import org.talend.sdk.component.dependencies.maven.Artifact; 079import org.talend.sdk.component.design.extension.DesignModel; 080import org.talend.sdk.component.runtime.base.Lifecycle; 081import org.talend.sdk.component.runtime.internationalization.ComponentBundle; 082import org.talend.sdk.component.runtime.internationalization.FamilyBundle; 083import org.talend.sdk.component.runtime.manager.ComponentFamilyMeta; 084import org.talend.sdk.component.runtime.manager.ComponentFamilyMeta.BaseMeta; 085import org.talend.sdk.component.runtime.manager.ComponentFamilyMeta.PartitionMapperMeta; 086import org.talend.sdk.component.runtime.manager.ComponentFamilyMeta.ProcessorMeta; 087import org.talend.sdk.component.runtime.manager.ComponentManager; 088import org.talend.sdk.component.runtime.manager.ContainerComponentRegistry; 089import org.talend.sdk.component.runtime.manager.extension.ComponentContexts; 090import org.talend.sdk.component.server.api.ComponentResource; 091import org.talend.sdk.component.server.configuration.ComponentServerConfiguration; 092import org.talend.sdk.component.server.dao.ComponentDao; 093import org.talend.sdk.component.server.dao.ComponentFamilyDao; 094import org.talend.sdk.component.server.front.base.internal.RequestKey; 095import org.talend.sdk.component.server.front.model.ComponentDetail; 096import org.talend.sdk.component.server.front.model.ComponentDetailList; 097import org.talend.sdk.component.server.front.model.ComponentId; 098import org.talend.sdk.component.server.front.model.ComponentIndex; 099import org.talend.sdk.component.server.front.model.ComponentIndices; 100import org.talend.sdk.component.server.front.model.Dependencies; 101import org.talend.sdk.component.server.front.model.DependencyDefinition; 102import org.talend.sdk.component.server.front.model.ErrorDictionary; 103import org.talend.sdk.component.server.front.model.Icon; 104import org.talend.sdk.component.server.front.model.IconSymbol; 105import org.talend.sdk.component.server.front.model.Link; 106import org.talend.sdk.component.server.front.model.SimplePropertyDefinition; 107import org.talend.sdk.component.server.front.model.error.ErrorPayload; 108import org.talend.sdk.component.server.front.security.SecurityUtils; 109import org.talend.sdk.component.server.lang.MapCache; 110import org.talend.sdk.component.server.service.ActionsService; 111import org.talend.sdk.component.server.service.ComponentManagerService; 112import org.talend.sdk.component.server.service.ExtensionComponentMetadataManager; 113import org.talend.sdk.component.server.service.IconResolver; 114import org.talend.sdk.component.server.service.LocaleMapper; 115import org.talend.sdk.component.server.service.PropertiesService; 116import org.talend.sdk.component.server.service.SimpleQueryLanguageCompiler; 117import org.talend.sdk.component.server.service.VirtualDependenciesService; 118import org.talend.sdk.component.server.service.event.DeployedComponent; 119import org.talend.sdk.component.server.service.jcache.FrontCacheKeyGenerator; 120import org.talend.sdk.component.server.service.jcache.FrontCacheResolver; 121import org.talend.sdk.component.spi.component.ComponentExtension; 122import org.w3c.dom.Document; 123import org.w3c.dom.Element; 124 125import lombok.extern.slf4j.Slf4j; 126 127@Slf4j 128@ApplicationScoped 129@CacheDefaults(cacheResolverFactory = FrontCacheResolver.class, cacheKeyGenerator = FrontCacheKeyGenerator.class) 130public class ComponentResourceImpl implements ComponentResource { 131 132 public static final String BASE64_PREFIX = "base64://"; //$NON-NLS-1$ 133 134 public static final String COMPONENT_TYPE_STANDALONE = "standalone"; 135 136 public static final String COMPONENT_TYPE_INPUT = "input"; 137 138 public static final String COMPONENT_TYPE_PROCESSOR = "processor"; 139 140 public static final String MSG_NO_PLUGIN = "No plugin '"; 141 142 public static final String MSG_NO_ICON_FOR_FAMILY = "No icon for family: "; 143 144 public static final String MSG_NO_FAMILY_FOR_IDENTIFIER = "No family for identifier: "; 145 146 public static final String THEME_DARK = "dark"; 147 148 public static final String THEME_LIGHT = "light"; 149 150 public static final String THEME_ALL = "all"; 151 152 public static final String IMAGE_SVG_XML_TYPE = "image/svg+xml"; 153 154 private final ConcurrentMap<RequestKey, ComponentIndices> indicesPerRequest = new ConcurrentHashMap<>(); 155 156 @Inject 157 private ComponentManager manager; 158 159 @Inject 160 private ComponentManagerService componentManagerService; 161 162 @Inject 163 private ComponentDao componentDao; 164 165 @Inject 166 private ComponentFamilyDao componentFamilyDao; 167 168 @Inject 169 private LocaleMapper localeMapper; 170 171 @Inject 172 private ActionsService actionsService; 173 174 @Inject 175 private PropertiesService propertiesService; 176 177 @Inject 178 private IconResolver iconResolver; 179 180 @Inject 181 private ComponentServerConfiguration configuration; 182 183 @Inject 184 private VirtualDependenciesService virtualDependenciesService; 185 186 @Inject 187 private ExtensionComponentMetadataManager virtualComponents; 188 189 @Inject 190 private MapCache caches; 191 192 @Inject 193 private SimpleQueryLanguageCompiler queryLanguageCompiler; 194 195 @Inject 196 @Context 197 private HttpHeaders headers; 198 199 @Inject 200 private SecurityUtils secUtils; 201 202 private String defaultTheme; 203 204 private final Map<String, Function<ComponentIndex, Object>> componentEvaluators = new HashMap<>(); 205 206 @PostConstruct 207 private void setupRuntime() { 208 log.info("[setupRuntime] Initializing " + getClass()); 209 defaultTheme = configuration.getIconDefaultTheme(); 210 final String themeMode = configuration.getSupportIconTheme() ? "themed" : "legacy"; 211 log.info("[setupRuntime] Icon mode: {}; default theme: {}.", themeMode, defaultTheme); 212 // preload some highly used data 213 getIndex("en", false, null, defaultTheme); 214 215 componentEvaluators.put("plugin", c -> c.getId().getPlugin()); 216 componentEvaluators.put("id", c -> c.getId().getId()); 217 componentEvaluators.put("familyId", c -> c.getId().getFamilyId()); 218 componentEvaluators.put("name", c -> c.getId().getName()); 219 componentEvaluators.put("metadata", component -> { 220 final Iterator<SimplePropertyDefinition> iterator = 221 getDetail("en", new String[] { component.getId().getId() }) 222 .getDetails() 223 .iterator() 224 .next() 225 .getProperties() 226 .iterator(); 227 if (iterator.hasNext()) { 228 return iterator.next().getMetadata(); 229 } 230 return emptyMap(); 231 }); 232 } 233 234 public void clearCache(@Observes final DeployedComponent deployedComponent) { 235 indicesPerRequest.clear(); 236 } 237 238 @Override 239 @CacheResult 240 public Dependencies getDependencies(final String[] ids) { 241 if (ids.length == 0) { 242 return new Dependencies(emptyMap()); 243 } 244 final Map<String, DependencyDefinition> dependencies = new HashMap<>(); 245 for (final String id : ids) { 246 if (virtualComponents.isExtensionEntity(id)) { 247 final DependencyDefinition deps = ofNullable(virtualComponents.getDependenciesFor(id)) 248 .orElseGet(() -> new DependencyDefinition(emptyList())); 249 dependencies.put(id, deps); 250 } else { 251 final ComponentFamilyMeta.BaseMeta<Lifecycle> meta = componentDao.findById(id); 252 253 if (meta == null) { 254 // Manage when the meta is null because of an unknown identifier 255 throw new WebApplicationException(Response 256 .status(Response.Status.NOT_FOUND) 257 .type(APPLICATION_JSON_TYPE) 258 .entity(new ErrorPayload(COMPONENT_MISSING, 259 "No component matching the id: " + id)) 260 .build()); 261 } 262 263 dependencies.put(meta.getId(), getDependenciesFor(meta)); 264 } 265 } 266 return new Dependencies(dependencies); 267 } 268 269 @Override 270 @CacheResult 271 public StreamingOutput getDependency(final String id) { 272 final ComponentFamilyMeta.BaseMeta<?> component = componentDao.findById(id); 273 final Supplier<InputStream> streamProvider; 274 if (component != null) { // local dep 275 final Path file = componentManagerService 276 .manager() 277 .findPlugin(component.getParent().getPlugin()) 278 .orElseThrow(() -> new WebApplicationException(Response 279 .status(Response.Status.NOT_FOUND) 280 .type(APPLICATION_JSON_TYPE) 281 .entity(new ErrorPayload(PLUGIN_MISSING, 282 "No plugin matching the id: " + id)) 283 .build())) 284 .getContainerFile() 285 .orElseThrow(() -> new WebApplicationException(Response 286 .status(Response.Status.NOT_FOUND) 287 .type(APPLICATION_JSON_TYPE) 288 .entity(new ErrorPayload(PLUGIN_MISSING, 289 "No dependency matching the id: " + id)) 290 .build())); 291 if (!Files.exists(file)) { 292 return onMissingJar(id); 293 } 294 streamProvider = () -> { 295 try { 296 return Files.newInputStream(file); 297 } catch (final IOException e) { 298 throw new IllegalStateException(e); 299 } 300 }; 301 } else { // just try to resolve it locally, note we would need to ensure some security here 302 final Artifact artifact = Artifact.from(id); 303 if (virtualDependenciesService.isVirtual(id)) { 304 streamProvider = virtualDependenciesService.retrieveArtifact(artifact); 305 if (streamProvider == null) { 306 return onMissingJar(id); 307 } 308 } else { 309 final Path file = componentManagerService.manager().getContainer().resolve(artifact.toPath()); 310 if (!Files.exists(file)) { 311 return onMissingJar(id); 312 } 313 streamProvider = () -> { 314 try { 315 return Files.newInputStream(file); 316 } catch (final IOException e) { 317 throw new IllegalStateException(e); 318 } 319 }; 320 } 321 } 322 return output -> { 323 final byte[] buffer = new byte[40960]; // 5k 324 try (final InputStream stream = new BufferedInputStream(streamProvider.get(), buffer.length)) { 325 int count; 326 while ((count = stream.read(buffer)) >= 0) { 327 if (count == 0) { 328 continue; 329 } 330 output.write(buffer, 0, count); 331 } 332 } 333 }; 334 } 335 336 @Override 337 @CacheResult 338 public ComponentIndices getIndex(final String language, final boolean includeIconContent, final String query, 339 final String theme) { 340 final Locale locale = localeMapper.mapLocale(language); 341 final String themedIcon = theme == null ? defaultTheme : theme; 342 caches.evictIfNeeded(indicesPerRequest, configuration.getMaxCacheSize() - 1); 343 return indicesPerRequest.computeIfAbsent(new RequestKey(locale, includeIconContent, query, themedIcon), k -> { 344 final Predicate<ComponentIndex> filter = queryLanguageCompiler.compile(query, componentEvaluators); 345 return new ComponentIndices(Stream 346 .concat(findDeployedComponents(includeIconContent, locale, themedIcon), virtualComponents 347 .getDetails() 348 .stream() 349 .map(detail -> new ComponentIndex( 350 detail.getId(), 351 detail.getDisplayName(), 352 detail.getId().getFamily(), 353 detail.getType(), 354 new Icon(detail.getIcon(), null, null, themedIcon), 355 new Icon(virtualComponents.getFamilyIconFor(detail.getId().getFamilyId()), null, 356 null, themedIcon), 357 detail.getVersion(), 358 singletonList(detail.getId().getFamily()), 359 detail.getLinks(), 360 detail.getMetadata()))) 361 .filter(filter) 362 .collect(toList())); 363 }); 364 } 365 366 @Override 367 @CacheResult 368 public Response familyIcon(final String id, final String theme) { 369 if (virtualComponents.isExtensionEntity(id)) { 370 return Response 371 .status(Response.Status.NOT_FOUND) 372 .entity(new ErrorPayload(ErrorDictionary.ICON_MISSING, MSG_NO_ICON_FOR_FAMILY + id)) 373 .type(APPLICATION_JSON_TYPE) 374 .build(); 375 } 376 377 final ComponentFamilyMeta meta = componentFamilyDao.findById(id); 378 if (meta == null) { 379 return Response 380 .status(Response.Status.NOT_FOUND) 381 .entity(new ErrorPayload(ErrorDictionary.FAMILY_MISSING, MSG_NO_FAMILY_FOR_IDENTIFIER + id)) 382 .type(APPLICATION_JSON_TYPE) 383 .build(); 384 } 385 final Optional<Container> plugin = manager.findPlugin(meta.getPlugin()); 386 if (!plugin.isPresent()) { 387 return Response 388 .status(Response.Status.NOT_FOUND) 389 .entity(new ErrorPayload(PLUGIN_MISSING, 390 MSG_NO_PLUGIN + meta.getPlugin() + "' for identifier: " + id)) 391 .type(APPLICATION_JSON_TYPE) 392 .build(); 393 } 394 395 final IconResolver.Icon iconContent = iconResolver.resolve(plugin.get(), meta.getIcon(), theme); 396 if (iconContent == null) { 397 return Response 398 .status(Response.Status.NOT_FOUND) 399 .entity(new ErrorPayload(ErrorDictionary.ICON_MISSING, "No icon for family identifier: " + id)) 400 .type(APPLICATION_JSON_TYPE) 401 .build(); 402 } 403 404 return Response.ok(iconContent.getBytes()).type(iconContent.getType()).build(); 405 } 406 407 @Override 408 @CacheResult 409 public Response icon(final String familyId, final String iconKey, final String theme) { 410 if (virtualComponents.isExtensionEntity(familyId)) { 411 return Response 412 .status(Response.Status.NOT_FOUND) 413 .entity(new ErrorPayload(ErrorDictionary.ICON_MISSING, MSG_NO_ICON_FOR_FAMILY + familyId)) 414 .type(APPLICATION_JSON_TYPE) 415 .build(); 416 } 417 418 final ComponentFamilyMeta meta = componentFamilyDao.findById(familyId); 419 if (meta == null) { 420 return Response 421 .status(Response.Status.NOT_FOUND) 422 .entity(new ErrorPayload(ErrorDictionary.FAMILY_MISSING, MSG_NO_FAMILY_FOR_IDENTIFIER + familyId)) 423 .type(APPLICATION_JSON_TYPE) 424 .build(); 425 } 426 final Optional<Container> plugin = manager.findPlugin(meta.getPlugin()); 427 if (!plugin.isPresent()) { 428 return Response 429 .status(Response.Status.NOT_FOUND) 430 .entity(new ErrorPayload(PLUGIN_MISSING, 431 MSG_NO_PLUGIN + meta.getPlugin() + "' for family identifier: " + familyId)) 432 .type(APPLICATION_JSON_TYPE) 433 .build(); 434 } 435 436 final IconResolver.Icon iconContent = iconResolver.resolve(plugin.get(), iconKey, theme); 437 if (iconContent == null) { 438 return Response 439 .status(Response.Status.NOT_FOUND) 440 .entity(new ErrorPayload(ErrorDictionary.ICON_MISSING, "No icon for icon key: " + iconKey)) 441 .type(APPLICATION_JSON_TYPE) 442 .build(); 443 } 444 445 return Response.ok(iconContent.getBytes()).type(iconContent.getType()).build(); 446 } 447 448 @Override 449 @CacheResult 450 public Response icon(final String id, final String theme) { 451 if (virtualComponents.isExtensionEntity(id)) { 452 return Response 453 .status(Response.Status.NOT_FOUND) 454 .entity(new ErrorPayload(ErrorDictionary.ICON_MISSING, MSG_NO_ICON_FOR_FAMILY + id)) 455 .type(APPLICATION_JSON_TYPE) 456 .build(); 457 } 458 459 final ComponentFamilyMeta.BaseMeta<Lifecycle> meta = componentDao.findById(id); 460 if (meta == null) { 461 return Response 462 .status(Response.Status.NOT_FOUND) 463 .entity(new ErrorPayload(COMPONENT_MISSING, "No component for identifier: " + id)) 464 .type(APPLICATION_JSON_TYPE) 465 .build(); 466 } 467 468 final Optional<Container> plugin = manager.findPlugin(meta.getParent().getPlugin()); 469 if (!plugin.isPresent()) { 470 return Response 471 .status(Response.Status.NOT_FOUND) 472 .entity(new ErrorPayload(PLUGIN_MISSING, 473 MSG_NO_PLUGIN + meta.getParent().getPlugin() + "' for identifier: " + id)) 474 .type(APPLICATION_JSON_TYPE) 475 .build(); 476 } 477 478 final IconResolver.Icon iconContent = iconResolver.resolve(plugin.get(), meta.getIcon(), theme); 479 if (iconContent == null) { 480 return Response 481 .status(Response.Status.NOT_FOUND) 482 .entity(new ErrorPayload(ErrorDictionary.ICON_MISSING, "No icon for identifier: " + id)) 483 .type(APPLICATION_JSON_TYPE) 484 .build(); 485 } 486 487 return Response.ok(iconContent.getBytes()).type(iconContent.getType()).build(); 488 } 489 490 @Override 491 @CacheResult 492 public Response getIconIndex(final String theme) { 493 final String themedIcon = theme == null ? defaultTheme : theme; 494 try { 495 final Map<String, IconSymbol> icons = collectIcons(themedIcon); 496 if (icons.isEmpty()) { 497 return Response 498 .status(Response.Status.NOT_FOUND) 499 .entity(new ErrorPayload(ErrorDictionary.ICON_MISSING, "No svg icon available")) 500 .type(APPLICATION_JSON_TYPE) 501 .build(); 502 } 503 // build the document. 504 final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 505 // to be compliant, prohibit the use of all protocols by external entities: 506 factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); 507 factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); 508 final Document doc = factory.newDocumentBuilder().newDocument(); 509 final Element root = doc.createElement("svg"); 510 root.setAttribute("xmlns", "http://www.w3.org/2000/svg"); 511 root.setAttribute("focusable", "false"); 512 root.setAttribute("class", "sr-only"); 513 root.setAttribute("data-theme", themedIcon); 514 doc.appendChild(root); 515 icons.values().forEach(icon -> { 516 final Element symbol = doc.createElement("symbol"); 517 symbol.setAttribute("id", String.format("%s-%s", icon.getIcon(), icon.getTheme())); 518 symbol.setAttribute("data-theme", icon.getTheme()); 519 symbol.setAttribute("data-type", icon.getType()); 520 symbol.setAttribute("data-family", icon.getFamily()); 521 if ("connector".equals(icon.getType())) { 522 symbol.setAttribute("data-connector", icon.getConnector()); 523 } 524 symbol.setTextContent(new String(icon.getContent())); 525 root.appendChild(symbol); 526 }); 527 final TransformerFactory transformerFactory = javax.xml.transform.TransformerFactory.newInstance(); 528 // to be compliant, prohibit the use of all protocols by external entities: 529 transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); 530 transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, ""); 531 final Transformer transformer = transformerFactory.newTransformer(); 532 transformer.setOutputProperty(OMIT_XML_DECLARATION, "yes"); 533 final Writer writer = new StringWriter(); 534 transformer.transform(new DOMSource(doc), new StreamResult(writer)); 535 final String svgs = writer.toString() 536 .replace("<", "<") 537 .replace(">", ">"); 538 539 return Response.ok(svgs).type(IMAGE_SVG_XML_TYPE).build(); 540 } catch (Exception e) { 541 log.error("[getIconIndex] {}", e.getMessage()); 542 return Response 543 .status(Status.INTERNAL_SERVER_ERROR) 544 .entity(new ErrorPayload(ErrorDictionary.UNEXPECTED, e.getMessage())) 545 .type(APPLICATION_JSON_TYPE) 546 .build(); 547 } 548 } 549 550 @Override 551 public Map<String, String> migrate(final String id, final int version, final Map<String, String> config) { 552 final Map<String, String> configuration = config.entrySet().stream().map(e -> { 553 if (e.getValue().startsWith(BASE64_PREFIX)) { 554 final String value = new String(Base64 555 .getUrlDecoder() 556 .decode(e.getValue().substring(BASE64_PREFIX.length()).getBytes(StandardCharsets.UTF_8))); 557 e.setValue(value); 558 } 559 return e; 560 }).collect(toMap(Entry::getKey, Entry::getValue)); 561 if (virtualComponents.isExtensionEntity(id)) { 562 return config; 563 } 564 final BaseMeta<Lifecycle> comp = ofNullable(componentDao.findById(id)) 565 .orElseThrow(() -> new WebApplicationException(Response 566 .status(Status.NOT_FOUND) 567 .entity(new ErrorPayload(COMPONENT_MISSING, "Didn't find component " + id)) 568 .build())); 569 if (version > comp.getVersion()) { 570 log.warn("[Component#migrate] Skipping {}#{} configuration migration due to incoming {} > registry {}.", 571 comp.getParent().getName(), comp.getName(), version, comp.getVersion()); 572 return config; 573 } 574 if (comp.getVersion() != version) { 575 log.info("[Component#migrate] {}#{} registry version {} - incoming: {}.", comp.getParent().getName(), 576 comp.getName(), comp.getVersion(), version); 577 } 578 return ofNullable(componentDao.findById(id)) 579 .orElseThrow(() -> new WebApplicationException(Response 580 .status(Response.Status.NOT_FOUND) 581 .entity(new ErrorPayload(COMPONENT_MISSING, "Didn't find component " + id)) 582 .build())) 583 .getMigrationHandler() 584 .get() 585 .migrate(version, config); 586 } 587 588 @Override 589 @CacheResult 590 public ComponentDetailList getDetail(final String language, final String[] ids) { 591 if (ids == null || ids.length == 0) { 592 return new ComponentDetailList(emptyList()); 593 } 594 595 final Map<String, ErrorPayload> errors = new HashMap<>(); 596 final List<ComponentDetail> details = Stream.of(ids).map(id -> { 597 if (virtualComponents.isExtensionEntity(id)) { 598 return virtualComponents.findComponentById(id).orElseGet(() -> { 599 errors.put(id, new ErrorPayload(COMPONENT_MISSING, "No virtual component '" + id + "'")); 600 return null; 601 }); 602 } 603 return ofNullable(componentDao.findById(id)).map(meta -> { 604 final Optional<Container> plugin = manager.findPlugin(meta.getParent().getPlugin()); 605 if (!plugin.isPresent()) { 606 errors 607 .put(meta.getId(), new ErrorPayload(PLUGIN_MISSING, 608 MSG_NO_PLUGIN + meta.getParent().getPlugin() + "'")); 609 return null; 610 } 611 612 final Container container = plugin.get(); 613 final Optional<DesignModel> model = ofNullable(meta.get(DesignModel.class)); 614 if (!model.isPresent()) { 615 errors 616 .put(meta.getId(), 617 new ErrorPayload(DESIGN_MODEL_MISSING, "No design model '" + meta.getId() + "'")); 618 return null; 619 } 620 621 final Locale locale = localeMapper.mapLocale(language); 622 final String type; 623 if (ProcessorMeta.class.isInstance(meta)) { 624 type = COMPONENT_TYPE_PROCESSOR; 625 } else if (PartitionMapperMeta.class.isInstance(meta)) { 626 type = COMPONENT_TYPE_INPUT; 627 } else { 628 type = COMPONENT_TYPE_STANDALONE; 629 } 630 631 final ComponentBundle bundle = meta.findBundle(container.getLoader(), locale); 632 final ComponentDetail componentDetail = new ComponentDetail(); 633 componentDetail.setLinks(emptyList()); 634 componentDetail.setId(createMetaId(container, meta)); 635 componentDetail.setVersion(meta.getVersion()); 636 componentDetail.setIcon(meta.getIcon()); 637 componentDetail.setInputFlows(model.get().getInputFlows()); 638 componentDetail.setOutputFlows(model.get().getOutputFlows()); 639 componentDetail.setType(type); 640 componentDetail.setDisplayName(bundle.displayName().orElse(meta.getName())); 641 componentDetail.setProperties(propertiesService 642 .buildProperties(meta.getParameterMetas().get(), container.getLoader(), locale, null) 643 .collect(toList())); 644 componentDetail.setActions(actionsService 645 .findActions(meta.getParent().getName(), container, locale, meta, 646 meta.getParent().findBundle(container.getLoader(), locale))); 647 componentDetail.setMetadata(translateMetadata(meta.getMetadata(), bundle)); 648 649 return componentDetail; 650 }).orElseGet(() -> { 651 errors.put(id, new ErrorPayload(COMPONENT_MISSING, "No component '" + id + "'")); 652 return null; 653 }); 654 }).filter(Objects::nonNull).collect(toList()); 655 656 if (!errors.isEmpty()) { 657 throw new WebApplicationException(Response.status(Response.Status.BAD_REQUEST).entity(errors).build()); 658 } 659 660 return new ComponentDetailList(details); 661 } 662 663 private Map<String, IconSymbol> collectIcons(final String theme) { 664 if (THEME_ALL.equals(theme)) { 665 Map<String, IconSymbol> icons = getAllIconsForTheme(THEME_LIGHT); 666 icons.putAll(getAllIconsForTheme(THEME_DARK)); 667 return icons; 668 } else { 669 return getAllIconsForTheme(theme); 670 } 671 } 672 673 private Map<String, IconSymbol> getAllIconsForTheme(final String theme) { 674 final ComponentIndices index = getIndex(Locale.ROOT.getLanguage(), true, null, theme); 675 try { 676 final List<ComponentIndex> components = index.getComponents(); 677 Map<String, IconSymbol> icons = components 678 .stream() 679 .filter(c -> c.getIconFamily().getCustomIcon() != null) 680 .filter(c -> IMAGE_SVG_XML_TYPE.equals(c.getIconFamily().getCustomIconType())) 681 .map(c -> new IconSymbol(c.getIconFamily().getIcon(), 682 c.getFamilyDisplayName(), 683 "family", 684 "", 685 theme, 686 c.getIconFamily().getCustomIcon())) 687 .collect(toMap(IconSymbol::getUid, identity(), (r1, r2) -> r1)); 688 icons.putAll(components 689 .stream() 690 .filter(c -> c.getIcon().getCustomIcon() != null) 691 .filter(c -> IMAGE_SVG_XML_TYPE.equals(c.getIcon().getCustomIconType())) 692 .map(c -> new IconSymbol(c.getIcon().getIcon(), 693 c.getFamilyDisplayName(), 694 "connector", 695 c.getDisplayName(), 696 theme, 697 c.getIcon().getCustomIcon())) 698 .collect(toMap(IconSymbol::getUid, identity(), (r1, r2) -> r1))); 699 return icons; 700 } catch (Exception e) { 701 log.error("[getAllIconsForTheme]", e); 702 throw e; 703 } 704 } 705 706 private Stream<ComponentIndex> findDeployedComponents(final boolean includeIconContent, final Locale locale, 707 final String theme) { 708 return manager 709 .find(c -> c 710 .execute(() -> c.get(ContainerComponentRegistry.class).getComponents().values().stream()) 711 .flatMap(component -> Stream 712 .of(component 713 .getPartitionMappers() 714 .values() 715 .stream() 716 .map(mapper -> toComponentIndex(c, locale, c.getId(), mapper, 717 c.get(ComponentManager.OriginalId.class), includeIconContent, 718 COMPONENT_TYPE_INPUT, theme)), 719 component 720 .getProcessors() 721 .values() 722 .stream() 723 .map(proc -> toComponentIndex(c, locale, c.getId(), proc, 724 c.get(ComponentManager.OriginalId.class), includeIconContent, 725 COMPONENT_TYPE_PROCESSOR, theme)), 726 component 727 .getDriverRunners() 728 .values() 729 .stream() 730 .map(runner -> toComponentIndex(c, locale, c.getId(), runner, 731 c.get(ComponentManager.OriginalId.class), includeIconContent, 732 COMPONENT_TYPE_STANDALONE, theme))) 733 .flatMap(Function.identity()))); 734 } 735 736 private DependencyDefinition getDependenciesFor(final ComponentFamilyMeta.BaseMeta<?> meta) { 737 final ComponentFamilyMeta familyMeta = meta.getParent(); 738 final Optional<Container> container = componentManagerService.manager().findPlugin(familyMeta.getPlugin()); 739 return new DependencyDefinition(container.map(c -> { 740 final ComponentExtension.ComponentContext context = 741 c.get(ComponentContexts.class).getContexts().get(meta.getType()); 742 final ComponentExtension extension = context.owningExtension(); 743 final Stream<Artifact> deps = c.findDependencies(); 744 final Stream<Artifact> artifacts; 745 if (configuration.getAddExtensionDependencies() && extension != null) { 746 final List<Artifact> dependencies = deps.collect(toList()); 747 final Stream<Artifact> addDeps = getExtensionDependencies(extension, dependencies); 748 artifacts = Stream.concat(dependencies.stream(), addDeps); 749 } else { 750 artifacts = deps; 751 } 752 return artifacts.map(Artifact::toCoordinate).collect(toList()); 753 }).orElseThrow(() -> new IllegalArgumentException("Can't find container '" + meta.getId() + "'"))); 754 } 755 756 private Stream<Artifact> getExtensionDependencies(final ComponentExtension extension, 757 final List<Artifact> filtered) { 758 return extension 759 .getAdditionalDependencies() 760 .stream() 761 .map(Artifact::from) 762 // filter required artifacts if they are already present in the list. 763 .filter(extArtifact -> filtered 764 .stream() 765 .map(d -> d.getGroup() + ":" + d.getArtifact()) 766 .noneMatch(ga -> ga.equals(extArtifact.getGroup() + ":" + extArtifact.getArtifact()))); 767 } 768 769 private ComponentId createMetaId(final Container container, final ComponentFamilyMeta.BaseMeta<Lifecycle> meta) { 770 return new ComponentId(meta.getId(), meta.getParent().getId(), meta.getParent().getPlugin(), 771 ofNullable(container.get(ComponentManager.OriginalId.class)) 772 .map(ComponentManager.OriginalId::getValue) 773 .orElse(container.getId()), 774 meta.getParent().getName(), meta.getName()); 775 } 776 777 private ComponentIndex toComponentIndex(final Container container, final Locale locale, final String plugin, 778 final ComponentFamilyMeta.BaseMeta meta, final ComponentManager.OriginalId originalId, 779 final boolean includeIcon, final String type, final String theme) { 780 final ClassLoader loader = container.getLoader(); 781 final String iconTheme = theme == null ? defaultTheme : theme; 782 final String icon = meta.getIcon(); 783 final String familyIcon = meta.getParent().getIcon(); 784 final IconResolver.Icon iconContent = iconResolver.resolve(container, icon, iconTheme); 785 final IconResolver.Icon iconFamilyContent = iconResolver.resolve(container, familyIcon, iconTheme); 786 final FamilyBundle parentBundle = meta.getParent().findBundle(loader, locale); 787 final ComponentBundle bundle = meta.findBundle(loader, locale); 788 final String familyDisplayName = parentBundle.displayName().orElse(meta.getParent().getName()); 789 final List<String> categories = ofNullable(meta.getParent().getCategories()) 790 .map(vals -> vals 791 .stream() 792 .map(this::normalizeCategory) 793 .map(category -> category.replace("${family}", meta.getParent().getName())) 794 .map(category -> parentBundle.category(category) 795 .orElseGet(() -> category.replace("/" + meta.getParent().getName() + "/", 796 "/" + familyDisplayName + "/"))) 797 .collect(toList())) 798 .orElseGet(Collections::emptyList); 799 return new ComponentIndex( 800 new ComponentId(meta.getId(), meta.getParent().getId(), plugin, 801 ofNullable(originalId).map(ComponentManager.OriginalId::getValue).orElse(plugin), 802 meta.getParent().getName(), meta.getName()), 803 bundle.displayName().orElse(meta.getName()), 804 familyDisplayName, type, 805 new Icon(icon, iconContent == null ? null : iconContent.getType(), 806 !includeIcon ? null : (iconContent == null ? null : iconContent.getBytes()), iconTheme), 807 new Icon(familyIcon, iconFamilyContent == null ? null : iconFamilyContent.getType(), 808 !includeIcon ? null : (iconFamilyContent == null ? null : iconFamilyContent.getBytes()), 809 iconTheme), 810 meta.getVersion(), 811 categories, 812 singletonList(new Link("Detail", "/component/details?identifiers=" + meta.getId(), 813 MediaType.APPLICATION_JSON)), 814 // 815 translateMetadata(meta.getMetadata(), bundle)); 816 } 817 818 private String normalizeCategory(final String category) { 819 // we prevent root categories and always append the family in this case 820 if (!category.contains("${family}")) { 821 return category + "/${family}"; 822 } 823 return category; 824 } 825 826 private Map<String, String> translateMetadata(final Map<String, String> source, final ComponentBundle bundle) { 827 return source 828 .entrySet() 829 .stream() 830 .map(e -> bundle.displayName(e.getKey().replaceAll("::", ".")) 831 .map(it -> (Entry<String, String>) new SimpleEntry(e.getKey(), it)) 832 .orElse(e)) 833 .map(t -> { 834 if (t.getKey().equals("documentation::value")) { 835 final String bundleDoc = bundle.documentation().orElse(null); 836 if (bundleDoc != null) { 837 t.setValue(bundleDoc); 838 } 839 } 840 return t; 841 }) 842 .collect(toMap(Entry::getKey, Entry::getValue)); 843 } 844 845 private StreamingOutput onMissingJar(final String id) { 846 throw new WebApplicationException(Response 847 .status(Response.Status.NOT_FOUND) 848 .type(APPLICATION_JSON_TYPE) 849 .entity(new ErrorPayload(PLUGIN_MISSING, "No file found for: " + id)) 850 .build()); 851 } 852}