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