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}