001/**
002 * Copyright (C) 2006-2022 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}