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