001/** 002 * Copyright (C) 2006-2023 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.stream.Collectors.toList; 023import static java.util.stream.Collectors.toMap; 024import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE; 025import static org.talend.sdk.component.server.front.model.ErrorDictionary.COMPONENT_MISSING; 026import static org.talend.sdk.component.server.front.model.ErrorDictionary.DESIGN_MODEL_MISSING; 027import static org.talend.sdk.component.server.front.model.ErrorDictionary.PLUGIN_MISSING; 028 029import java.io.BufferedInputStream; 030import java.io.IOException; 031import java.io.InputStream; 032import java.nio.charset.StandardCharsets; 033import java.nio.file.Files; 034import java.nio.file.Path; 035import java.util.AbstractMap.SimpleEntry; 036import java.util.Base64; 037import java.util.Collections; 038import java.util.HashMap; 039import java.util.Iterator; 040import java.util.List; 041import java.util.Locale; 042import java.util.Map; 043import java.util.Map.Entry; 044import java.util.Objects; 045import java.util.Optional; 046import java.util.concurrent.ConcurrentHashMap; 047import java.util.concurrent.ConcurrentMap; 048import java.util.function.Function; 049import java.util.function.Predicate; 050import java.util.function.Supplier; 051import java.util.stream.Stream; 052 053import javax.annotation.PostConstruct; 054import javax.cache.annotation.CacheDefaults; 055import javax.cache.annotation.CacheResult; 056import javax.enterprise.context.ApplicationScoped; 057import javax.enterprise.event.Observes; 058import javax.inject.Inject; 059import javax.ws.rs.WebApplicationException; 060import javax.ws.rs.core.Context; 061import javax.ws.rs.core.HttpHeaders; 062import javax.ws.rs.core.MediaType; 063import javax.ws.rs.core.Response; 064import javax.ws.rs.core.Response.Status; 065import javax.ws.rs.core.StreamingOutput; 066 067import org.talend.sdk.component.container.Container; 068import org.talend.sdk.component.dependencies.maven.Artifact; 069import org.talend.sdk.component.design.extension.DesignModel; 070import org.talend.sdk.component.runtime.base.Lifecycle; 071import org.talend.sdk.component.runtime.internationalization.ComponentBundle; 072import org.talend.sdk.component.runtime.internationalization.FamilyBundle; 073import org.talend.sdk.component.runtime.manager.ComponentFamilyMeta; 074import org.talend.sdk.component.runtime.manager.ComponentFamilyMeta.BaseMeta; 075import org.talend.sdk.component.runtime.manager.ComponentFamilyMeta.PartitionMapperMeta; 076import org.talend.sdk.component.runtime.manager.ComponentFamilyMeta.ProcessorMeta; 077import org.talend.sdk.component.runtime.manager.ComponentManager; 078import org.talend.sdk.component.runtime.manager.ContainerComponentRegistry; 079import org.talend.sdk.component.runtime.manager.extension.ComponentContexts; 080import org.talend.sdk.component.server.api.ComponentResource; 081import org.talend.sdk.component.server.configuration.ComponentServerConfiguration; 082import org.talend.sdk.component.server.dao.ComponentDao; 083import org.talend.sdk.component.server.dao.ComponentFamilyDao; 084import org.talend.sdk.component.server.front.base.internal.RequestKey; 085import org.talend.sdk.component.server.front.model.ComponentDetail; 086import org.talend.sdk.component.server.front.model.ComponentDetailList; 087import org.talend.sdk.component.server.front.model.ComponentId; 088import org.talend.sdk.component.server.front.model.ComponentIndex; 089import org.talend.sdk.component.server.front.model.ComponentIndices; 090import org.talend.sdk.component.server.front.model.Dependencies; 091import org.talend.sdk.component.server.front.model.DependencyDefinition; 092import org.talend.sdk.component.server.front.model.ErrorDictionary; 093import org.talend.sdk.component.server.front.model.Icon; 094import org.talend.sdk.component.server.front.model.Link; 095import org.talend.sdk.component.server.front.model.SimplePropertyDefinition; 096import org.talend.sdk.component.server.front.model.error.ErrorPayload; 097import org.talend.sdk.component.server.front.security.SecurityUtils; 098import org.talend.sdk.component.server.lang.MapCache; 099import org.talend.sdk.component.server.service.ActionsService; 100import org.talend.sdk.component.server.service.ComponentManagerService; 101import org.talend.sdk.component.server.service.ExtensionComponentMetadataManager; 102import org.talend.sdk.component.server.service.IconResolver; 103import org.talend.sdk.component.server.service.LocaleMapper; 104import org.talend.sdk.component.server.service.PropertiesService; 105import org.talend.sdk.component.server.service.SimpleQueryLanguageCompiler; 106import org.talend.sdk.component.server.service.VirtualDependenciesService; 107import org.talend.sdk.component.server.service.event.DeployedComponent; 108import org.talend.sdk.component.server.service.jcache.FrontCacheKeyGenerator; 109import org.talend.sdk.component.server.service.jcache.FrontCacheResolver; 110import org.talend.sdk.component.spi.component.ComponentExtension; 111 112import lombok.extern.slf4j.Slf4j; 113 114@Slf4j 115@ApplicationScoped 116@CacheDefaults(cacheResolverFactory = FrontCacheResolver.class, cacheKeyGenerator = FrontCacheKeyGenerator.class) 117public class ComponentResourceImpl implements ComponentResource { 118 119 public static final String BASE64_PREFIX = "base64://"; //$NON-NLS-1$ 120 121 public static final String COMPONENT_TYPE_STANDALONE = "standalone"; 122 123 public static final String COMPONENT_TYPE_INPUT = "input"; 124 125 public static final String COMPONENT_TYPE_PROCESSOR = "processor"; 126 127 private final ConcurrentMap<RequestKey, ComponentIndices> indicesPerRequest = new ConcurrentHashMap<>(); 128 129 @Inject 130 private ComponentManager manager; 131 132 @Inject 133 private ComponentManagerService componentManagerService; 134 135 @Inject 136 private ComponentDao componentDao; 137 138 @Inject 139 private ComponentFamilyDao componentFamilyDao; 140 141 @Inject 142 private LocaleMapper localeMapper; 143 144 @Inject 145 private ActionsService actionsService; 146 147 @Inject 148 private PropertiesService propertiesService; 149 150 @Inject 151 private IconResolver iconResolver; 152 153 @Inject 154 private ComponentServerConfiguration configuration; 155 156 @Inject 157 private VirtualDependenciesService virtualDependenciesService; 158 159 @Inject 160 private ExtensionComponentMetadataManager virtualComponents; 161 162 @Inject 163 private MapCache caches; 164 165 @Inject 166 private SimpleQueryLanguageCompiler queryLanguageCompiler; 167 168 @Inject 169 @Context 170 private HttpHeaders headers; 171 172 @Inject 173 private SecurityUtils secUtils; 174 175 private final Map<String, Function<ComponentIndex, Object>> componentEvaluators = new HashMap<>(); 176 177 @PostConstruct 178 private void setupRuntime() { 179 log.info("Initializing " + getClass()); 180 181 // preload some highly used data 182 getIndex("en", false, null); 183 184 componentEvaluators.put("plugin", c -> c.getId().getPlugin()); 185 componentEvaluators.put("id", c -> c.getId().getId()); 186 componentEvaluators.put("familyId", c -> c.getId().getFamilyId()); 187 componentEvaluators.put("name", c -> c.getId().getName()); 188 componentEvaluators.put("metadata", component -> { 189 final Iterator<SimplePropertyDefinition> iterator = 190 getDetail("en", new String[] { component.getId().getId() }) 191 .getDetails() 192 .iterator() 193 .next() 194 .getProperties() 195 .iterator(); 196 if (iterator.hasNext()) { 197 return iterator.next().getMetadata(); 198 } 199 return emptyMap(); 200 }); 201 } 202 203 public void clearCache(@Observes final DeployedComponent deployedComponent) { 204 indicesPerRequest.clear(); 205 } 206 207 @Override 208 @CacheResult 209 public Dependencies getDependencies(final String[] ids) { 210 if (ids.length == 0) { 211 return new Dependencies(emptyMap()); 212 } 213 final Map<String, DependencyDefinition> dependencies = new HashMap<>(); 214 for (final String id : ids) { 215 if (virtualComponents.isExtensionEntity(id)) { 216 final DependencyDefinition deps = ofNullable(virtualComponents.getDependenciesFor(id)) 217 .orElseGet(() -> new DependencyDefinition(emptyList())); 218 dependencies.put(id, deps); 219 } else { 220 final ComponentFamilyMeta.BaseMeta<Lifecycle> meta = componentDao.findById(id); 221 dependencies.put(meta.getId(), getDependenciesFor(meta)); 222 } 223 } 224 return new Dependencies(dependencies); 225 } 226 227 @Override 228 @CacheResult 229 public StreamingOutput getDependency(final String id) { 230 final ComponentFamilyMeta.BaseMeta<?> component = componentDao.findById(id); 231 final Supplier<InputStream> streamProvider; 232 if (component != null) { // local dep 233 final Path file = componentManagerService 234 .manager() 235 .findPlugin(component.getParent().getPlugin()) 236 .orElseThrow(() -> new WebApplicationException(Response 237 .status(Response.Status.NOT_FOUND) 238 .type(APPLICATION_JSON_TYPE) 239 .entity(new ErrorPayload(PLUGIN_MISSING, "No plugin matching the id: " + id)) 240 .build())) 241 .getContainerFile() 242 .orElseThrow(() -> new WebApplicationException(Response 243 .status(Response.Status.NOT_FOUND) 244 .type(APPLICATION_JSON_TYPE) 245 .entity(new ErrorPayload(PLUGIN_MISSING, "No dependency matching the id: " + id)) 246 .build())); 247 if (!Files.exists(file)) { 248 return onMissingJar(id); 249 } 250 streamProvider = () -> { 251 try { 252 return Files.newInputStream(file); 253 } catch (final IOException e) { 254 throw new IllegalStateException(e); 255 } 256 }; 257 } else { // just try to resolve it locally, note we would need to ensure some security here 258 final Artifact artifact = Artifact.from(id); 259 if (virtualDependenciesService.isVirtual(id)) { 260 streamProvider = virtualDependenciesService.retrieveArtifact(artifact); 261 if (streamProvider == null) { 262 return onMissingJar(id); 263 } 264 } else { 265 final Path file = componentManagerService.manager().getContainer().resolve(artifact.toPath()); 266 if (!Files.exists(file)) { 267 return onMissingJar(id); 268 } 269 streamProvider = () -> { 270 try { 271 return Files.newInputStream(file); 272 } catch (final IOException e) { 273 throw new IllegalStateException(e); 274 } 275 }; 276 } 277 } 278 return output -> { 279 final byte[] buffer = new byte[40960]; // 5k 280 try (final InputStream stream = new BufferedInputStream(streamProvider.get(), buffer.length)) { 281 int count; 282 while ((count = stream.read(buffer)) >= 0) { 283 if (count == 0) { 284 continue; 285 } 286 output.write(buffer, 0, count); 287 } 288 } 289 }; 290 } 291 292 @Override 293 @CacheResult 294 public ComponentIndices getIndex(final String language, final boolean includeIconContent, final String query) { 295 final Locale locale = localeMapper.mapLocale(language); 296 caches.evictIfNeeded(indicesPerRequest, configuration.getMaxCacheSize() - 1); 297 return indicesPerRequest.computeIfAbsent(new RequestKey(locale, includeIconContent, query), k -> { 298 final Predicate<ComponentIndex> filter = queryLanguageCompiler.compile(query, componentEvaluators); 299 return new ComponentIndices(Stream 300 .concat(findDeployedComponents(includeIconContent, locale), virtualComponents 301 .getDetails() 302 .stream() 303 .map(detail -> new ComponentIndex( 304 detail.getId(), 305 detail.getDisplayName(), 306 detail.getId().getFamily(), 307 detail.getType(), 308 new Icon(detail.getIcon(), null, null), 309 new Icon(virtualComponents.getFamilyIconFor(detail.getId().getFamilyId()), null, 310 null), 311 detail.getVersion(), 312 singletonList(detail.getId().getFamily()), 313 detail.getLinks(), 314 detail.getMetadata()))) 315 .filter(filter) 316 .collect(toList())); 317 }); 318 } 319 320 @Override 321 @CacheResult 322 public Response familyIcon(final String id) { 323 if (virtualComponents.isExtensionEntity(id)) { // todo or just use front bundle? 324 return Response 325 .status(Response.Status.NOT_FOUND) 326 .entity(new ErrorPayload(ErrorDictionary.ICON_MISSING, "No icon for family: " + id)) 327 .type(APPLICATION_JSON_TYPE) 328 .build(); 329 } 330 331 // todo: add caching if SvgIconResolver becomes used a lot - not the case ATM 332 final ComponentFamilyMeta meta = componentFamilyDao.findById(id); 333 if (meta == null) { 334 return Response 335 .status(Response.Status.NOT_FOUND) 336 .entity(new ErrorPayload(ErrorDictionary.FAMILY_MISSING, "No family for identifier: " + id)) 337 .type(APPLICATION_JSON_TYPE) 338 .build(); 339 } 340 final Optional<Container> plugin = manager.findPlugin(meta.getPlugin()); 341 if (!plugin.isPresent()) { 342 return Response 343 .status(Response.Status.NOT_FOUND) 344 .entity(new ErrorPayload(PLUGIN_MISSING, 345 "No plugin '" + meta.getPlugin() + "' for identifier: " + id)) 346 .type(APPLICATION_JSON_TYPE) 347 .build(); 348 } 349 350 final IconResolver.Icon iconContent = iconResolver.resolve(plugin.get(), meta.getIcon()); 351 if (iconContent == null) { 352 return Response 353 .status(Response.Status.NOT_FOUND) 354 .entity(new ErrorPayload(ErrorDictionary.ICON_MISSING, "No icon for family identifier: " + id)) 355 .type(APPLICATION_JSON_TYPE) 356 .build(); 357 } 358 359 return Response.ok(iconContent.getBytes()).type(iconContent.getType()).build(); 360 } 361 362 @Override 363 @CacheResult 364 public Response icon(final String id) { 365 if (virtualComponents.isExtensionEntity(id)) { // todo if the front bundle is not sufficient 366 return Response 367 .status(Response.Status.NOT_FOUND) 368 .entity(new ErrorPayload(ErrorDictionary.ICON_MISSING, "No icon for family: " + id)) 369 .type(APPLICATION_JSON_TYPE) 370 .build(); 371 } 372 373 // todo: add caching if SvgIconResolver becomes used a lot - not the case ATM 374 final ComponentFamilyMeta.BaseMeta<Lifecycle> meta = componentDao.findById(id); 375 if (meta == null) { 376 return Response 377 .status(Response.Status.NOT_FOUND) 378 .entity(new ErrorPayload(COMPONENT_MISSING, "No component for identifier: " + id)) 379 .type(APPLICATION_JSON_TYPE) 380 .build(); 381 } 382 383 final Optional<Container> plugin = manager.findPlugin(meta.getParent().getPlugin()); 384 if (!plugin.isPresent()) { 385 return Response 386 .status(Response.Status.NOT_FOUND) 387 .entity(new ErrorPayload(PLUGIN_MISSING, 388 "No plugin '" + meta.getParent().getPlugin() + "' for identifier: " + id)) 389 .type(APPLICATION_JSON_TYPE) 390 .build(); 391 } 392 393 final IconResolver.Icon iconContent = iconResolver.resolve(plugin.get(), meta.getIcon()); 394 if (iconContent == null) { 395 return Response 396 .status(Response.Status.NOT_FOUND) 397 .entity(new ErrorPayload(ErrorDictionary.ICON_MISSING, "No icon for identifier: " + id)) 398 .type(APPLICATION_JSON_TYPE) 399 .build(); 400 } 401 402 return Response.ok(iconContent.getBytes()).type(iconContent.getType()).build(); 403 } 404 405 @Override 406 public Map<String, String> migrate(final String id, final int version, final Map<String, String> config) { 407 final Map<String, String> configuration = config.entrySet().stream().map(e -> { 408 if (e.getValue().startsWith(BASE64_PREFIX)) { 409 final String value = new String(Base64 410 .getUrlDecoder() 411 .decode(e.getValue().substring(BASE64_PREFIX.length()).getBytes(StandardCharsets.UTF_8))); 412 e.setValue(value); 413 } 414 return e; 415 }).collect(toMap(Entry::getKey, Entry::getValue)); 416 String tenant; 417 try { 418 tenant = headers.getHeaderString("x-talend-tenant-id"); 419 } catch (Exception e) { 420 log.debug("[migrate] context not applicable: {}", e.getMessage()); 421 tenant = null; 422 } 423 if (virtualComponents.isExtensionEntity(id)) { 424 return config; 425 } 426 final BaseMeta<Lifecycle> comp = ofNullable(componentDao.findById(id)) 427 .orElseThrow(() -> new WebApplicationException(Response 428 .status(Status.NOT_FOUND) 429 .entity(new ErrorPayload(COMPONENT_MISSING, "Didn't find component " + id)) 430 .build())); 431 final Map<String, String> decrypted = secUtils.decrypt(comp.getParameterMetas().get(), configuration, tenant); 432 433 return ofNullable(componentDao.findById(id)) 434 .orElseThrow(() -> new WebApplicationException(Response 435 .status(Response.Status.NOT_FOUND) 436 .entity(new ErrorPayload(COMPONENT_MISSING, "Didn't find component " + id)) 437 .build())) 438 .getMigrationHandler() 439 .get() 440 .migrate(version, decrypted); 441 } 442 443 @Override // TODO: max ids.length 444 @CacheResult 445 public ComponentDetailList getDetail(final String language, final String[] ids) { 446 if (ids == null || ids.length == 0) { 447 return new ComponentDetailList(emptyList()); 448 } 449 450 final Map<String, ErrorPayload> errors = new HashMap<>(); 451 final List<ComponentDetail> details = Stream.of(ids).map(id -> { 452 if (virtualComponents.isExtensionEntity(id)) { 453 return virtualComponents.findComponentById(id).orElseGet(() -> { 454 errors.put(id, new ErrorPayload(COMPONENT_MISSING, "No virtual component '" + id + "'")); 455 return null; 456 }); 457 } 458 return ofNullable(componentDao.findById(id)).map(meta -> { 459 final Optional<Container> plugin = manager.findPlugin(meta.getParent().getPlugin()); 460 if (!plugin.isPresent()) { 461 errors 462 .put(meta.getId(), new ErrorPayload(PLUGIN_MISSING, 463 "No plugin '" + meta.getParent().getPlugin() + "'")); 464 return null; 465 } 466 467 final Container container = plugin.get(); 468 final Optional<DesignModel> model = ofNullable(meta.get(DesignModel.class)); 469 if (!model.isPresent()) { 470 errors 471 .put(meta.getId(), 472 new ErrorPayload(DESIGN_MODEL_MISSING, "No design model '" + meta.getId() + "'")); 473 return null; 474 } 475 476 final Locale locale = localeMapper.mapLocale(language); 477 final String type; 478 if (ProcessorMeta.class.isInstance(meta)) { 479 type = COMPONENT_TYPE_PROCESSOR; 480 } else if (PartitionMapperMeta.class.isInstance(meta)) { 481 type = COMPONENT_TYPE_INPUT; 482 } else { 483 type = COMPONENT_TYPE_STANDALONE; 484 } 485 486 final ComponentBundle bundle = meta.findBundle(container.getLoader(), locale); 487 final ComponentDetail componentDetail = new ComponentDetail(); 488 componentDetail.setLinks(emptyList() /* todo ? */); 489 componentDetail.setId(createMetaId(container, meta)); 490 componentDetail.setVersion(meta.getVersion()); 491 componentDetail.setIcon(meta.getIcon()); 492 componentDetail.setInputFlows(model.get().getInputFlows()); 493 componentDetail.setOutputFlows(model.get().getOutputFlows()); 494 componentDetail.setType(type); 495 componentDetail.setDisplayName(bundle.displayName().orElse(meta.getName())); 496 componentDetail.setProperties(propertiesService 497 .buildProperties(meta.getParameterMetas().get(), container.getLoader(), locale, null) 498 .collect(toList())); 499 componentDetail.setActions(actionsService 500 .findActions(meta.getParent().getName(), container, locale, meta, 501 meta.getParent().findBundle(container.getLoader(), locale))); 502 componentDetail.setMetadata(translateMetadata(meta.getMetadata(), bundle)); 503 504 return componentDetail; 505 }).orElseGet(() -> { 506 errors.put(id, new ErrorPayload(COMPONENT_MISSING, "No component '" + id + "'")); 507 return null; 508 }); 509 }).filter(Objects::nonNull).collect(toList()); 510 511 if (!errors.isEmpty()) { 512 throw new WebApplicationException(Response.status(Response.Status.BAD_REQUEST).entity(errors).build()); 513 } 514 515 return new ComponentDetailList(details); 516 } 517 518 private Stream<ComponentIndex> findDeployedComponents(final boolean includeIconContent, final Locale locale) { 519 return manager 520 .find(c -> c 521 .execute(() -> c.get(ContainerComponentRegistry.class).getComponents().values().stream()) 522 .flatMap(component -> Stream 523 .of(component 524 .getPartitionMappers() 525 .values() 526 .stream() 527 .map(mapper -> toComponentIndex(c, locale, c.getId(), mapper, 528 c.get(ComponentManager.OriginalId.class), includeIconContent, 529 COMPONENT_TYPE_INPUT)), 530 component 531 .getProcessors() 532 .values() 533 .stream() 534 .map(proc -> toComponentIndex(c, locale, c.getId(), proc, 535 c.get(ComponentManager.OriginalId.class), includeIconContent, 536 COMPONENT_TYPE_PROCESSOR)), 537 component 538 .getDriverRunners() 539 .values() 540 .stream() 541 .map(runner -> toComponentIndex(c, locale, c.getId(), runner, 542 c.get(ComponentManager.OriginalId.class), includeIconContent, 543 COMPONENT_TYPE_STANDALONE))) 544 .flatMap(Function.identity()))); 545 } 546 547 private DependencyDefinition getDependenciesFor(final ComponentFamilyMeta.BaseMeta<?> meta) { 548 final ComponentFamilyMeta familyMeta = meta.getParent(); 549 final Optional<Container> container = componentManagerService.manager().findPlugin(familyMeta.getPlugin()); 550 return new DependencyDefinition(container.map(c -> { 551 final ComponentExtension.ComponentContext context = 552 c.get(ComponentContexts.class).getContexts().get(meta.getType()); 553 final ComponentExtension extension = context.owningExtension(); 554 final Stream<Artifact> deps = c.findDependencies(); 555 final Stream<Artifact> artifacts; 556 if (configuration.getAddExtensionDependencies() && extension != null) { 557 final List<Artifact> dependencies = deps.collect(toList()); 558 final Stream<Artifact> addDeps = getExtensionDependencies(extension, dependencies); 559 artifacts = Stream.concat(dependencies.stream(), addDeps); 560 } else { 561 artifacts = deps; 562 } 563 return artifacts.map(Artifact::toCoordinate).collect(toList()); 564 }).orElseThrow(() -> new IllegalArgumentException("Can't find container '" + meta.getId() + "'"))); 565 } 566 567 private Stream<Artifact> getExtensionDependencies(final ComponentExtension extension, 568 final List<Artifact> filtered) { 569 return extension 570 .getAdditionalDependencies() 571 .stream() 572 .map(Artifact::from) 573 // filter required artifacts if they are already present in the list. 574 .filter(extArtifact -> filtered 575 .stream() 576 .map(d -> d.getGroup() + ":" + d.getArtifact()) 577 .noneMatch(ga -> ga.equals(extArtifact.getGroup() + ":" + extArtifact.getArtifact()))); 578 } 579 580 private ComponentId createMetaId(final Container container, final ComponentFamilyMeta.BaseMeta<Lifecycle> meta) { 581 return new ComponentId(meta.getId(), meta.getParent().getId(), meta.getParent().getPlugin(), 582 ofNullable(container.get(ComponentManager.OriginalId.class)) 583 .map(ComponentManager.OriginalId::getValue) 584 .orElse(container.getId()), 585 meta.getParent().getName(), meta.getName()); 586 } 587 588 private ComponentIndex toComponentIndex(final Container container, final Locale locale, final String plugin, 589 final ComponentFamilyMeta.BaseMeta meta, final ComponentManager.OriginalId originalId, 590 final boolean includeIcon, final String type) { 591 final ClassLoader loader = container.getLoader(); 592 final String icon = meta.getIcon(); 593 final String familyIcon = meta.getParent().getIcon(); 594 final IconResolver.Icon iconContent = iconResolver.resolve(container, icon); 595 final IconResolver.Icon iconFamilyContent = iconResolver.resolve(container, familyIcon); 596 final FamilyBundle parentBundle = meta.getParent().findBundle(loader, locale); 597 final ComponentBundle bundle = meta.findBundle(loader, locale); 598 final String familyDisplayName = parentBundle.displayName().orElse(meta.getParent().getName()); 599 final List<String> categories = ofNullable(meta.getParent().getCategories()) 600 .map(vals -> vals 601 .stream() 602 .map(this::normalizeCategory) 603 .map(category -> category.replace("${family}", meta.getParent().getName())) 604 .map(category -> parentBundle.category(category) 605 .orElseGet(() -> category.replace("/" + meta.getParent().getName() + "/", 606 "/" + familyDisplayName + "/"))) 607 .collect(toList())) 608 .orElseGet(Collections::emptyList); 609 return new ComponentIndex( 610 new ComponentId(meta.getId(), meta.getParent().getId(), plugin, 611 ofNullable(originalId).map(ComponentManager.OriginalId::getValue).orElse(plugin), 612 meta.getParent().getName(), meta.getName()), 613 bundle.displayName().orElse(meta.getName()), 614 familyDisplayName, type, 615 new Icon(icon, iconContent == null ? null : iconContent.getType(), 616 !includeIcon ? null : (iconContent == null ? null : iconContent.getBytes())), 617 new Icon(familyIcon, iconFamilyContent == null ? null : iconFamilyContent.getType(), 618 !includeIcon ? null : (iconFamilyContent == null ? null : iconFamilyContent.getBytes())), 619 meta.getVersion(), 620 categories, 621 singletonList(new Link("Detail", "/component/details?identifiers=" + meta.getId(), 622 MediaType.APPLICATION_JSON)), 623 // 624 translateMetadata(meta.getMetadata(), bundle)); 625 } 626 627 private String normalizeCategory(final String category) { 628 // we prevent root categories and always append the family in this case 629 if (!category.contains("${family}")) { 630 return category + "/${family}"; 631 } 632 return category; 633 } 634 635 private Map<String, String> translateMetadata(final Map<String, String> source, final ComponentBundle bundle) { 636 return source 637 .entrySet() 638 .stream() 639 .map(e -> bundle.displayName(e.getKey().replaceAll("::", ".")) 640 .map(it -> (Entry<String, String>) new SimpleEntry(e.getKey(), it)) 641 .orElse(e)) 642 .map(t -> { 643 if (t.getKey().equals("documentation::value")) { 644 final String bundleDoc = bundle.documentation().orElse(null); 645 if (bundleDoc != null) { 646 t.setValue(bundleDoc); 647 } 648 } 649 return t; 650 }) 651 .collect(toMap(Entry::getKey, Entry::getValue)); 652 } 653 654 private StreamingOutput onMissingJar(final String id) { 655 throw new WebApplicationException(Response 656 .status(Response.Status.NOT_FOUND) 657 .type(APPLICATION_JSON_TYPE) 658 .entity(new ErrorPayload(PLUGIN_MISSING, "No file found for: " + id)) 659 .build()); 660 } 661}