001/**
002 * Copyright (C) 2006-2025 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.function.UnaryOperator.identity;
023import static java.util.stream.Collectors.toList;
024import static java.util.stream.Collectors.toMap;
025import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE;
026import static javax.xml.transform.OutputKeys.OMIT_XML_DECLARATION;
027import static org.talend.sdk.component.server.front.model.ErrorDictionary.COMPONENT_MISSING;
028import static org.talend.sdk.component.server.front.model.ErrorDictionary.DESIGN_MODEL_MISSING;
029import static org.talend.sdk.component.server.front.model.ErrorDictionary.PLUGIN_MISSING;
030
031import java.io.BufferedInputStream;
032import java.io.IOException;
033import java.io.InputStream;
034import java.io.StringWriter;
035import java.io.Writer;
036import java.nio.charset.StandardCharsets;
037import java.nio.file.Files;
038import java.nio.file.Path;
039import java.util.AbstractMap.SimpleEntry;
040import java.util.Base64;
041import java.util.Collections;
042import java.util.HashMap;
043import java.util.Iterator;
044import java.util.List;
045import java.util.Locale;
046import java.util.Map;
047import java.util.Map.Entry;
048import java.util.Objects;
049import java.util.Optional;
050import java.util.concurrent.ConcurrentHashMap;
051import java.util.concurrent.ConcurrentMap;
052import java.util.function.Function;
053import java.util.function.Predicate;
054import java.util.function.Supplier;
055import java.util.stream.Stream;
056
057import javax.annotation.PostConstruct;
058import javax.cache.annotation.CacheDefaults;
059import javax.cache.annotation.CacheResult;
060import javax.enterprise.context.ApplicationScoped;
061import javax.enterprise.event.Observes;
062import javax.inject.Inject;
063import javax.ws.rs.WebApplicationException;
064import javax.ws.rs.core.Context;
065import javax.ws.rs.core.HttpHeaders;
066import javax.ws.rs.core.MediaType;
067import javax.ws.rs.core.Response;
068import javax.ws.rs.core.Response.Status;
069import javax.ws.rs.core.StreamingOutput;
070import javax.xml.XMLConstants;
071import javax.xml.parsers.DocumentBuilderFactory;
072import javax.xml.transform.Transformer;
073import javax.xml.transform.TransformerFactory;
074import javax.xml.transform.dom.DOMSource;
075import javax.xml.transform.stream.StreamResult;
076
077import org.talend.sdk.component.container.Container;
078import org.talend.sdk.component.dependencies.maven.Artifact;
079import org.talend.sdk.component.design.extension.DesignModel;
080import org.talend.sdk.component.runtime.base.Lifecycle;
081import org.talend.sdk.component.runtime.internationalization.ComponentBundle;
082import org.talend.sdk.component.runtime.internationalization.FamilyBundle;
083import org.talend.sdk.component.runtime.manager.ComponentFamilyMeta;
084import org.talend.sdk.component.runtime.manager.ComponentFamilyMeta.BaseMeta;
085import org.talend.sdk.component.runtime.manager.ComponentFamilyMeta.PartitionMapperMeta;
086import org.talend.sdk.component.runtime.manager.ComponentFamilyMeta.ProcessorMeta;
087import org.talend.sdk.component.runtime.manager.ComponentManager;
088import org.talend.sdk.component.runtime.manager.ContainerComponentRegistry;
089import org.talend.sdk.component.runtime.manager.extension.ComponentContexts;
090import org.talend.sdk.component.server.api.ComponentResource;
091import org.talend.sdk.component.server.configuration.ComponentServerConfiguration;
092import org.talend.sdk.component.server.dao.ComponentDao;
093import org.talend.sdk.component.server.dao.ComponentFamilyDao;
094import org.talend.sdk.component.server.front.base.internal.RequestKey;
095import org.talend.sdk.component.server.front.model.ComponentDetail;
096import org.talend.sdk.component.server.front.model.ComponentDetailList;
097import org.talend.sdk.component.server.front.model.ComponentId;
098import org.talend.sdk.component.server.front.model.ComponentIndex;
099import org.talend.sdk.component.server.front.model.ComponentIndices;
100import org.talend.sdk.component.server.front.model.Dependencies;
101import org.talend.sdk.component.server.front.model.DependencyDefinition;
102import org.talend.sdk.component.server.front.model.ErrorDictionary;
103import org.talend.sdk.component.server.front.model.Icon;
104import org.talend.sdk.component.server.front.model.IconSymbol;
105import org.talend.sdk.component.server.front.model.Link;
106import org.talend.sdk.component.server.front.model.SimplePropertyDefinition;
107import org.talend.sdk.component.server.front.model.error.ErrorPayload;
108import org.talend.sdk.component.server.front.security.SecurityUtils;
109import org.talend.sdk.component.server.lang.MapCache;
110import org.talend.sdk.component.server.service.ActionsService;
111import org.talend.sdk.component.server.service.ComponentManagerService;
112import org.talend.sdk.component.server.service.ExtensionComponentMetadataManager;
113import org.talend.sdk.component.server.service.IconResolver;
114import org.talend.sdk.component.server.service.LocaleMapper;
115import org.talend.sdk.component.server.service.PropertiesService;
116import org.talend.sdk.component.server.service.SimpleQueryLanguageCompiler;
117import org.talend.sdk.component.server.service.VirtualDependenciesService;
118import org.talend.sdk.component.server.service.event.DeployedComponent;
119import org.talend.sdk.component.server.service.jcache.FrontCacheKeyGenerator;
120import org.talend.sdk.component.server.service.jcache.FrontCacheResolver;
121import org.talend.sdk.component.spi.component.ComponentExtension;
122import org.w3c.dom.Document;
123import org.w3c.dom.Element;
124
125import lombok.extern.slf4j.Slf4j;
126
127@Slf4j
128@ApplicationScoped
129@CacheDefaults(cacheResolverFactory = FrontCacheResolver.class, cacheKeyGenerator = FrontCacheKeyGenerator.class)
130public class ComponentResourceImpl implements ComponentResource {
131
132    public static final String BASE64_PREFIX = "base64://"; //$NON-NLS-1$
133
134    public static final String COMPONENT_TYPE_STANDALONE = "standalone";
135
136    public static final String COMPONENT_TYPE_INPUT = "input";
137
138    public static final String COMPONENT_TYPE_PROCESSOR = "processor";
139
140    public static final String MSG_NO_PLUGIN = "No plugin '";
141
142    public static final String MSG_NO_ICON_FOR_FAMILY = "No icon for family: ";
143
144    public static final String MSG_NO_FAMILY_FOR_IDENTIFIER = "No family for identifier: ";
145
146    public static final String THEME_DARK = "dark";
147
148    public static final String THEME_LIGHT = "light";
149
150    public static final String THEME_ALL = "all";
151
152    public static final String IMAGE_SVG_XML_TYPE = "image/svg+xml";
153
154    private final ConcurrentMap<RequestKey, ComponentIndices> indicesPerRequest = new ConcurrentHashMap<>();
155
156    @Inject
157    private ComponentManager manager;
158
159    @Inject
160    private ComponentManagerService componentManagerService;
161
162    @Inject
163    private ComponentDao componentDao;
164
165    @Inject
166    private ComponentFamilyDao componentFamilyDao;
167
168    @Inject
169    private LocaleMapper localeMapper;
170
171    @Inject
172    private ActionsService actionsService;
173
174    @Inject
175    private PropertiesService propertiesService;
176
177    @Inject
178    private IconResolver iconResolver;
179
180    @Inject
181    private ComponentServerConfiguration configuration;
182
183    @Inject
184    private VirtualDependenciesService virtualDependenciesService;
185
186    @Inject
187    private ExtensionComponentMetadataManager virtualComponents;
188
189    @Inject
190    private MapCache caches;
191
192    @Inject
193    private SimpleQueryLanguageCompiler queryLanguageCompiler;
194
195    @Inject
196    @Context
197    private HttpHeaders headers;
198
199    @Inject
200    private SecurityUtils secUtils;
201
202    private String defaultTheme;
203
204    private final Map<String, Function<ComponentIndex, Object>> componentEvaluators = new HashMap<>();
205
206    @PostConstruct
207    private void setupRuntime() {
208        log.info("[setupRuntime] Initializing " + getClass());
209        defaultTheme = configuration.getIconDefaultTheme();
210        final String themeMode = configuration.getSupportIconTheme() ? "themed" : "legacy";
211        log.info("[setupRuntime] Icon mode: {}; default theme: {}.", themeMode, defaultTheme);
212        // preload some highly used data
213        getIndex("en", false, null, defaultTheme);
214
215        componentEvaluators.put("plugin", c -> c.getId().getPlugin());
216        componentEvaluators.put("id", c -> c.getId().getId());
217        componentEvaluators.put("familyId", c -> c.getId().getFamilyId());
218        componentEvaluators.put("name", c -> c.getId().getName());
219        componentEvaluators.put("metadata", component -> {
220            final Iterator<SimplePropertyDefinition> iterator =
221                    getDetail("en", new String[] { component.getId().getId() })
222                            .getDetails()
223                            .iterator()
224                            .next()
225                            .getProperties()
226                            .iterator();
227            if (iterator.hasNext()) {
228                return iterator.next().getMetadata();
229            }
230            return emptyMap();
231        });
232    }
233
234    public void clearCache(@Observes final DeployedComponent deployedComponent) {
235        indicesPerRequest.clear();
236    }
237
238    @Override
239    @CacheResult
240    public Dependencies getDependencies(final String[] ids) {
241        if (ids.length == 0) {
242            return new Dependencies(emptyMap());
243        }
244        final Map<String, DependencyDefinition> dependencies = new HashMap<>();
245        for (final String id : ids) {
246            if (virtualComponents.isExtensionEntity(id)) {
247                final DependencyDefinition deps = ofNullable(virtualComponents.getDependenciesFor(id))
248                        .orElseGet(() -> new DependencyDefinition(emptyList()));
249                dependencies.put(id, deps);
250            } else {
251                final ComponentFamilyMeta.BaseMeta<Lifecycle> meta = componentDao.findById(id);
252
253                if (meta == null) {
254                    // Manage when the meta is null because of an unknown identifier
255                    throw new WebApplicationException(Response
256                            .status(Response.Status.NOT_FOUND)
257                            .type(APPLICATION_JSON_TYPE)
258                            .entity(new ErrorPayload(COMPONENT_MISSING,
259                                    "No component matching the id: " + id))
260                            .build());
261                }
262
263                dependencies.put(meta.getId(), getDependenciesFor(meta));
264            }
265        }
266        return new Dependencies(dependencies);
267    }
268
269    @Override
270    @CacheResult
271    public StreamingOutput getDependency(final String id) {
272        final ComponentFamilyMeta.BaseMeta<?> component = componentDao.findById(id);
273        final Supplier<InputStream> streamProvider;
274        if (component != null) { // local dep
275            final Path file = componentManagerService
276                    .manager()
277                    .findPlugin(component.getParent().getPlugin())
278                    .orElseThrow(() -> new WebApplicationException(Response
279                            .status(Response.Status.NOT_FOUND)
280                            .type(APPLICATION_JSON_TYPE)
281                            .entity(new ErrorPayload(PLUGIN_MISSING,
282                                    "No plugin matching the id: " + id))
283                            .build()))
284                    .getContainerFile()
285                    .orElseThrow(() -> new WebApplicationException(Response
286                            .status(Response.Status.NOT_FOUND)
287                            .type(APPLICATION_JSON_TYPE)
288                            .entity(new ErrorPayload(PLUGIN_MISSING,
289                                    "No dependency matching the id: " + id))
290                            .build()));
291            if (!Files.exists(file)) {
292                return onMissingJar(id);
293            }
294            streamProvider = () -> {
295                try {
296                    return Files.newInputStream(file);
297                } catch (final IOException e) {
298                    throw new IllegalStateException(e);
299                }
300            };
301        } else { // just try to resolve it locally, note we would need to ensure some security here
302            final Artifact artifact = Artifact.from(id);
303            if (virtualDependenciesService.isVirtual(id)) {
304                streamProvider = virtualDependenciesService.retrieveArtifact(artifact);
305                if (streamProvider == null) {
306                    return onMissingJar(id);
307                }
308            } else {
309                final Path file = componentManagerService.manager().getContainer().resolve(artifact.toPath());
310                if (!Files.exists(file)) {
311                    return onMissingJar(id);
312                }
313                streamProvider = () -> {
314                    try {
315                        return Files.newInputStream(file);
316                    } catch (final IOException e) {
317                        throw new IllegalStateException(e);
318                    }
319                };
320            }
321        }
322        return output -> {
323            final byte[] buffer = new byte[40960]; // 5k
324            try (final InputStream stream = new BufferedInputStream(streamProvider.get(), buffer.length)) {
325                int count;
326                while ((count = stream.read(buffer)) >= 0) {
327                    if (count == 0) {
328                        continue;
329                    }
330                    output.write(buffer, 0, count);
331                }
332            }
333        };
334    }
335
336    @Override
337    @CacheResult
338    public ComponentIndices getIndex(final String language, final boolean includeIconContent, final String query,
339            final String theme) {
340        final Locale locale = localeMapper.mapLocale(language);
341        final String themedIcon = theme == null ? defaultTheme : theme;
342        caches.evictIfNeeded(indicesPerRequest, configuration.getMaxCacheSize() - 1);
343        return indicesPerRequest.computeIfAbsent(new RequestKey(locale, includeIconContent, query, themedIcon), k -> {
344            final Predicate<ComponentIndex> filter = queryLanguageCompiler.compile(query, componentEvaluators);
345            return new ComponentIndices(Stream
346                    .concat(findDeployedComponents(includeIconContent, locale, themedIcon), virtualComponents
347                            .getDetails()
348                            .stream()
349                            .map(detail -> new ComponentIndex(
350                                    detail.getId(),
351                                    detail.getDisplayName(),
352                                    detail.getId().getFamily(),
353                                    detail.getType(),
354                                    new Icon(detail.getIcon(), null, null, themedIcon),
355                                    new Icon(virtualComponents.getFamilyIconFor(detail.getId().getFamilyId()), null,
356                                            null, themedIcon),
357                                    detail.getVersion(),
358                                    singletonList(detail.getId().getFamily()),
359                                    detail.getLinks(),
360                                    detail.getMetadata())))
361                    .filter(filter)
362                    .collect(toList()));
363        });
364    }
365
366    @Override
367    @CacheResult
368    public Response familyIcon(final String id, final String theme) {
369        if (virtualComponents.isExtensionEntity(id)) {
370            return Response
371                    .status(Response.Status.NOT_FOUND)
372                    .entity(new ErrorPayload(ErrorDictionary.ICON_MISSING, MSG_NO_ICON_FOR_FAMILY + id))
373                    .type(APPLICATION_JSON_TYPE)
374                    .build();
375        }
376
377        final ComponentFamilyMeta meta = componentFamilyDao.findById(id);
378        if (meta == null) {
379            return Response
380                    .status(Response.Status.NOT_FOUND)
381                    .entity(new ErrorPayload(ErrorDictionary.FAMILY_MISSING, MSG_NO_FAMILY_FOR_IDENTIFIER + id))
382                    .type(APPLICATION_JSON_TYPE)
383                    .build();
384        }
385        final Optional<Container> plugin = manager.findPlugin(meta.getPlugin());
386        if (!plugin.isPresent()) {
387            return Response
388                    .status(Response.Status.NOT_FOUND)
389                    .entity(new ErrorPayload(PLUGIN_MISSING,
390                            MSG_NO_PLUGIN + meta.getPlugin() + "' for identifier: " + id))
391                    .type(APPLICATION_JSON_TYPE)
392                    .build();
393        }
394
395        final IconResolver.Icon iconContent = iconResolver.resolve(plugin.get(), meta.getIcon(), theme);
396        if (iconContent == null) {
397            return Response
398                    .status(Response.Status.NOT_FOUND)
399                    .entity(new ErrorPayload(ErrorDictionary.ICON_MISSING, "No icon for family identifier: " + id))
400                    .type(APPLICATION_JSON_TYPE)
401                    .build();
402        }
403
404        return Response.ok(iconContent.getBytes()).type(iconContent.getType()).build();
405    }
406
407    @Override
408    @CacheResult
409    public Response icon(final String familyId, final String iconKey, final String theme) {
410        if (virtualComponents.isExtensionEntity(familyId)) {
411            return Response
412                    .status(Response.Status.NOT_FOUND)
413                    .entity(new ErrorPayload(ErrorDictionary.ICON_MISSING, MSG_NO_ICON_FOR_FAMILY + familyId))
414                    .type(APPLICATION_JSON_TYPE)
415                    .build();
416        }
417
418        final ComponentFamilyMeta meta = componentFamilyDao.findById(familyId);
419        if (meta == null) {
420            return Response
421                    .status(Response.Status.NOT_FOUND)
422                    .entity(new ErrorPayload(ErrorDictionary.FAMILY_MISSING, MSG_NO_FAMILY_FOR_IDENTIFIER + familyId))
423                    .type(APPLICATION_JSON_TYPE)
424                    .build();
425        }
426        final Optional<Container> plugin = manager.findPlugin(meta.getPlugin());
427        if (!plugin.isPresent()) {
428            return Response
429                    .status(Response.Status.NOT_FOUND)
430                    .entity(new ErrorPayload(PLUGIN_MISSING,
431                            MSG_NO_PLUGIN + meta.getPlugin() + "' for family identifier: " + familyId))
432                    .type(APPLICATION_JSON_TYPE)
433                    .build();
434        }
435
436        final IconResolver.Icon iconContent = iconResolver.resolve(plugin.get(), iconKey, theme);
437        if (iconContent == null) {
438            return Response
439                    .status(Response.Status.NOT_FOUND)
440                    .entity(new ErrorPayload(ErrorDictionary.ICON_MISSING, "No icon for icon key: " + iconKey))
441                    .type(APPLICATION_JSON_TYPE)
442                    .build();
443        }
444
445        return Response.ok(iconContent.getBytes()).type(iconContent.getType()).build();
446    }
447
448    @Override
449    @CacheResult
450    public Response icon(final String id, final String theme) {
451        if (virtualComponents.isExtensionEntity(id)) {
452            return Response
453                    .status(Response.Status.NOT_FOUND)
454                    .entity(new ErrorPayload(ErrorDictionary.ICON_MISSING, MSG_NO_ICON_FOR_FAMILY + id))
455                    .type(APPLICATION_JSON_TYPE)
456                    .build();
457        }
458
459        final ComponentFamilyMeta.BaseMeta<Lifecycle> meta = componentDao.findById(id);
460        if (meta == null) {
461            return Response
462                    .status(Response.Status.NOT_FOUND)
463                    .entity(new ErrorPayload(COMPONENT_MISSING, "No component for identifier: " + id))
464                    .type(APPLICATION_JSON_TYPE)
465                    .build();
466        }
467
468        final Optional<Container> plugin = manager.findPlugin(meta.getParent().getPlugin());
469        if (!plugin.isPresent()) {
470            return Response
471                    .status(Response.Status.NOT_FOUND)
472                    .entity(new ErrorPayload(PLUGIN_MISSING,
473                            MSG_NO_PLUGIN + meta.getParent().getPlugin() + "' for identifier: " + id))
474                    .type(APPLICATION_JSON_TYPE)
475                    .build();
476        }
477
478        final IconResolver.Icon iconContent = iconResolver.resolve(plugin.get(), meta.getIcon(), theme);
479        if (iconContent == null) {
480            return Response
481                    .status(Response.Status.NOT_FOUND)
482                    .entity(new ErrorPayload(ErrorDictionary.ICON_MISSING, "No icon for identifier: " + id))
483                    .type(APPLICATION_JSON_TYPE)
484                    .build();
485        }
486
487        return Response.ok(iconContent.getBytes()).type(iconContent.getType()).build();
488    }
489
490    @Override
491    @CacheResult
492    public Response getIconIndex(final String theme) {
493        final String themedIcon = theme == null ? defaultTheme : theme;
494        try {
495            final Map<String, IconSymbol> icons = collectIcons(themedIcon);
496            if (icons.isEmpty()) {
497                return Response
498                        .status(Response.Status.NOT_FOUND)
499                        .entity(new ErrorPayload(ErrorDictionary.ICON_MISSING, "No svg icon available"))
500                        .type(APPLICATION_JSON_TYPE)
501                        .build();
502            }
503            // build the document.
504            final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
505            // to be compliant, prohibit the use of all protocols by external entities:
506            factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
507            factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
508            final Document doc = factory.newDocumentBuilder().newDocument();
509            final Element root = doc.createElement("svg");
510            root.setAttribute("xmlns", "http://www.w3.org/2000/svg");
511            root.setAttribute("focusable", "false");
512            root.setAttribute("class", "sr-only");
513            root.setAttribute("data-theme", themedIcon);
514            doc.appendChild(root);
515            icons.values().forEach(icon -> {
516                final Element symbol = doc.createElement("symbol");
517                symbol.setAttribute("id", String.format("%s-%s", icon.getIcon(), icon.getTheme()));
518                symbol.setAttribute("data-theme", icon.getTheme());
519                symbol.setAttribute("data-type", icon.getType());
520                symbol.setAttribute("data-family", icon.getFamily());
521                if ("connector".equals(icon.getType())) {
522                    symbol.setAttribute("data-connector", icon.getConnector());
523                }
524                symbol.setTextContent(new String(icon.getContent()));
525                root.appendChild(symbol);
526            });
527            final TransformerFactory transformerFactory = javax.xml.transform.TransformerFactory.newInstance();
528            // to be compliant, prohibit the use of all protocols by external entities:
529            transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
530            transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");
531            final Transformer transformer = transformerFactory.newTransformer();
532            transformer.setOutputProperty(OMIT_XML_DECLARATION, "yes");
533            final Writer writer = new StringWriter();
534            transformer.transform(new DOMSource(doc), new StreamResult(writer));
535            final String svgs = writer.toString()
536                    .replace("&lt;", "<")
537                    .replace("&gt;", ">");
538
539            return Response.ok(svgs).type(IMAGE_SVG_XML_TYPE).build();
540        } catch (Exception e) {
541            log.error("[getIconIndex] {}", e.getMessage());
542            return Response
543                    .status(Status.INTERNAL_SERVER_ERROR)
544                    .entity(new ErrorPayload(ErrorDictionary.UNEXPECTED, e.getMessage()))
545                    .type(APPLICATION_JSON_TYPE)
546                    .build();
547        }
548    }
549
550    @Override
551    public Map<String, String> migrate(final String id, final int version, final Map<String, String> config) {
552        final Map<String, String> configuration = config.entrySet().stream().map(e -> {
553            if (e.getValue().startsWith(BASE64_PREFIX)) {
554                final String value = new String(Base64
555                        .getUrlDecoder()
556                        .decode(e.getValue().substring(BASE64_PREFIX.length()).getBytes(StandardCharsets.UTF_8)));
557                e.setValue(value);
558            }
559            return e;
560        }).collect(toMap(Entry::getKey, Entry::getValue));
561        if (virtualComponents.isExtensionEntity(id)) {
562            return config;
563        }
564        final BaseMeta<Lifecycle> comp = ofNullable(componentDao.findById(id))
565                .orElseThrow(() -> new WebApplicationException(Response
566                        .status(Status.NOT_FOUND)
567                        .entity(new ErrorPayload(COMPONENT_MISSING, "Didn't find component " + id))
568                        .build()));
569        if (version > comp.getVersion()) {
570            log.warn("[Component#migrate] Skipping {}#{} configuration migration due to incoming {} > registry {}.",
571                    comp.getParent().getName(), comp.getName(), version, comp.getVersion());
572            return config;
573        }
574        if (comp.getVersion() != version) {
575            log.info("[Component#migrate] {}#{} registry version {} - incoming: {}.", comp.getParent().getName(),
576                    comp.getName(), comp.getVersion(), version);
577        }
578        return ofNullable(componentDao.findById(id))
579                .orElseThrow(() -> new WebApplicationException(Response
580                        .status(Response.Status.NOT_FOUND)
581                        .entity(new ErrorPayload(COMPONENT_MISSING, "Didn't find component " + id))
582                        .build()))
583                .getMigrationHandler()
584                .get()
585                .migrate(version, config);
586    }
587
588    @Override
589    @CacheResult
590    public ComponentDetailList getDetail(final String language, final String[] ids) {
591        if (ids == null || ids.length == 0) {
592            return new ComponentDetailList(emptyList());
593        }
594
595        final Map<String, ErrorPayload> errors = new HashMap<>();
596        final List<ComponentDetail> details = Stream.of(ids).map(id -> {
597            if (virtualComponents.isExtensionEntity(id)) {
598                return virtualComponents.findComponentById(id).orElseGet(() -> {
599                    errors.put(id, new ErrorPayload(COMPONENT_MISSING, "No virtual component '" + id + "'"));
600                    return null;
601                });
602            }
603            return ofNullable(componentDao.findById(id)).map(meta -> {
604                final Optional<Container> plugin = manager.findPlugin(meta.getParent().getPlugin());
605                if (!plugin.isPresent()) {
606                    errors
607                            .put(meta.getId(), new ErrorPayload(PLUGIN_MISSING,
608                                    MSG_NO_PLUGIN + meta.getParent().getPlugin() + "'"));
609                    return null;
610                }
611
612                final Container container = plugin.get();
613                final Optional<DesignModel> model = ofNullable(meta.get(DesignModel.class));
614                if (!model.isPresent()) {
615                    errors
616                            .put(meta.getId(),
617                                    new ErrorPayload(DESIGN_MODEL_MISSING, "No design model '" + meta.getId() + "'"));
618                    return null;
619                }
620
621                final Locale locale = localeMapper.mapLocale(language);
622                final String type;
623                if (ProcessorMeta.class.isInstance(meta)) {
624                    type = COMPONENT_TYPE_PROCESSOR;
625                } else if (PartitionMapperMeta.class.isInstance(meta)) {
626                    type = COMPONENT_TYPE_INPUT;
627                } else {
628                    type = COMPONENT_TYPE_STANDALONE;
629                }
630
631                final ComponentBundle bundle = meta.findBundle(container.getLoader(), locale);
632                final ComponentDetail componentDetail = new ComponentDetail();
633                componentDetail.setLinks(emptyList());
634                componentDetail.setId(createMetaId(container, meta));
635                componentDetail.setVersion(meta.getVersion());
636                componentDetail.setIcon(meta.getIcon());
637                componentDetail.setInputFlows(model.get().getInputFlows());
638                componentDetail.setOutputFlows(model.get().getOutputFlows());
639                componentDetail.setType(type);
640                componentDetail.setDisplayName(bundle.displayName().orElse(meta.getName()));
641                componentDetail.setProperties(propertiesService
642                        .buildProperties(meta.getParameterMetas().get(), container.getLoader(), locale, null)
643                        .collect(toList()));
644                componentDetail.setActions(actionsService
645                        .findActions(meta.getParent().getName(), container, locale, meta,
646                                meta.getParent().findBundle(container.getLoader(), locale)));
647                componentDetail.setMetadata(translateMetadata(meta.getMetadata(), bundle));
648
649                return componentDetail;
650            }).orElseGet(() -> {
651                errors.put(id, new ErrorPayload(COMPONENT_MISSING, "No component '" + id + "'"));
652                return null;
653            });
654        }).filter(Objects::nonNull).collect(toList());
655
656        if (!errors.isEmpty()) {
657            throw new WebApplicationException(Response.status(Response.Status.BAD_REQUEST).entity(errors).build());
658        }
659
660        return new ComponentDetailList(details);
661    }
662
663    private Map<String, IconSymbol> collectIcons(final String theme) {
664        if (THEME_ALL.equals(theme)) {
665            Map<String, IconSymbol> icons = getAllIconsForTheme(THEME_LIGHT);
666            icons.putAll(getAllIconsForTheme(THEME_DARK));
667            return icons;
668        } else {
669            return getAllIconsForTheme(theme);
670        }
671    }
672
673    private Map<String, IconSymbol> getAllIconsForTheme(final String theme) {
674        final ComponentIndices index = getIndex(Locale.ROOT.getLanguage(), true, null, theme);
675        try {
676            final List<ComponentIndex> components = index.getComponents();
677            Map<String, IconSymbol> icons = components
678                    .stream()
679                    .filter(c -> c.getIconFamily().getCustomIcon() != null)
680                    .filter(c -> IMAGE_SVG_XML_TYPE.equals(c.getIconFamily().getCustomIconType()))
681                    .map(c -> new IconSymbol(c.getIconFamily().getIcon(),
682                            c.getFamilyDisplayName(),
683                            "family",
684                            "",
685                            theme,
686                            c.getIconFamily().getCustomIcon()))
687                    .collect(toMap(IconSymbol::getUid, identity(), (r1, r2) -> r1));
688            icons.putAll(components
689                    .stream()
690                    .filter(c -> c.getIcon().getCustomIcon() != null)
691                    .filter(c -> IMAGE_SVG_XML_TYPE.equals(c.getIcon().getCustomIconType()))
692                    .map(c -> new IconSymbol(c.getIcon().getIcon(),
693                            c.getFamilyDisplayName(),
694                            "connector",
695                            c.getDisplayName(),
696                            theme,
697                            c.getIcon().getCustomIcon()))
698                    .collect(toMap(IconSymbol::getUid, identity(), (r1, r2) -> r1)));
699            return icons;
700        } catch (Exception e) {
701            log.error("[getAllIconsForTheme]", e);
702            throw e;
703        }
704    }
705
706    private Stream<ComponentIndex> findDeployedComponents(final boolean includeIconContent, final Locale locale,
707            final String theme) {
708        return manager
709                .find(c -> c
710                        .execute(() -> c.get(ContainerComponentRegistry.class).getComponents().values().stream())
711                        .flatMap(component -> Stream
712                                .of(component
713                                        .getPartitionMappers()
714                                        .values()
715                                        .stream()
716                                        .map(mapper -> toComponentIndex(c, locale, c.getId(), mapper,
717                                                c.get(ComponentManager.OriginalId.class), includeIconContent,
718                                                COMPONENT_TYPE_INPUT, theme)),
719                                        component
720                                                .getProcessors()
721                                                .values()
722                                                .stream()
723                                                .map(proc -> toComponentIndex(c, locale, c.getId(), proc,
724                                                        c.get(ComponentManager.OriginalId.class), includeIconContent,
725                                                        COMPONENT_TYPE_PROCESSOR, theme)),
726                                        component
727                                                .getDriverRunners()
728                                                .values()
729                                                .stream()
730                                                .map(runner -> toComponentIndex(c, locale, c.getId(), runner,
731                                                        c.get(ComponentManager.OriginalId.class), includeIconContent,
732                                                        COMPONENT_TYPE_STANDALONE, theme)))
733                                .flatMap(Function.identity())));
734    }
735
736    private DependencyDefinition getDependenciesFor(final ComponentFamilyMeta.BaseMeta<?> meta) {
737        final ComponentFamilyMeta familyMeta = meta.getParent();
738        final Optional<Container> container = componentManagerService.manager().findPlugin(familyMeta.getPlugin());
739        return new DependencyDefinition(container.map(c -> {
740            final ComponentExtension.ComponentContext context =
741                    c.get(ComponentContexts.class).getContexts().get(meta.getType());
742            final ComponentExtension extension = context.owningExtension();
743            final Stream<Artifact> deps = c.findDependencies();
744            final Stream<Artifact> artifacts;
745            if (configuration.getAddExtensionDependencies() && extension != null) {
746                final List<Artifact> dependencies = deps.collect(toList());
747                final Stream<Artifact> addDeps = getExtensionDependencies(extension, dependencies);
748                artifacts = Stream.concat(dependencies.stream(), addDeps);
749            } else {
750                artifacts = deps;
751            }
752            return artifacts.map(Artifact::toCoordinate).collect(toList());
753        }).orElseThrow(() -> new IllegalArgumentException("Can't find container '" + meta.getId() + "'")));
754    }
755
756    private Stream<Artifact> getExtensionDependencies(final ComponentExtension extension,
757            final List<Artifact> filtered) {
758        return extension
759                .getAdditionalDependencies()
760                .stream()
761                .map(Artifact::from)
762                // filter required artifacts if they are already present in the list.
763                .filter(extArtifact -> filtered
764                        .stream()
765                        .map(d -> d.getGroup() + ":" + d.getArtifact())
766                        .noneMatch(ga -> ga.equals(extArtifact.getGroup() + ":" + extArtifact.getArtifact())));
767    }
768
769    private ComponentId createMetaId(final Container container, final ComponentFamilyMeta.BaseMeta<Lifecycle> meta) {
770        return new ComponentId(meta.getId(), meta.getParent().getId(), meta.getParent().getPlugin(),
771                ofNullable(container.get(ComponentManager.OriginalId.class))
772                        .map(ComponentManager.OriginalId::getValue)
773                        .orElse(container.getId()),
774                meta.getParent().getName(), meta.getName());
775    }
776
777    private ComponentIndex toComponentIndex(final Container container, final Locale locale, final String plugin,
778            final ComponentFamilyMeta.BaseMeta meta, final ComponentManager.OriginalId originalId,
779            final boolean includeIcon, final String type, final String theme) {
780        final ClassLoader loader = container.getLoader();
781        final String iconTheme = theme == null ? defaultTheme : theme;
782        final String icon = meta.getIcon();
783        final String familyIcon = meta.getParent().getIcon();
784        final IconResolver.Icon iconContent = iconResolver.resolve(container, icon, iconTheme);
785        final IconResolver.Icon iconFamilyContent = iconResolver.resolve(container, familyIcon, iconTheme);
786        final FamilyBundle parentBundle = meta.getParent().findBundle(loader, locale);
787        final ComponentBundle bundle = meta.findBundle(loader, locale);
788        final String familyDisplayName = parentBundle.displayName().orElse(meta.getParent().getName());
789        final List<String> categories = ofNullable(meta.getParent().getCategories())
790                .map(vals -> vals
791                        .stream()
792                        .map(this::normalizeCategory)
793                        .map(category -> category.replace("${family}", meta.getParent().getName()))
794                        .map(category -> parentBundle.category(category)
795                                .orElseGet(() -> category.replace("/" + meta.getParent().getName() + "/",
796                                        "/" + familyDisplayName + "/")))
797                        .collect(toList()))
798                .orElseGet(Collections::emptyList);
799        return new ComponentIndex(
800                new ComponentId(meta.getId(), meta.getParent().getId(), plugin,
801                        ofNullable(originalId).map(ComponentManager.OriginalId::getValue).orElse(plugin),
802                        meta.getParent().getName(), meta.getName()),
803                bundle.displayName().orElse(meta.getName()),
804                familyDisplayName, type,
805                new Icon(icon, iconContent == null ? null : iconContent.getType(),
806                        !includeIcon ? null : (iconContent == null ? null : iconContent.getBytes()), iconTheme),
807                new Icon(familyIcon, iconFamilyContent == null ? null : iconFamilyContent.getType(),
808                        !includeIcon ? null : (iconFamilyContent == null ? null : iconFamilyContent.getBytes()),
809                        iconTheme),
810                meta.getVersion(),
811                categories,
812                singletonList(new Link("Detail", "/component/details?identifiers=" + meta.getId(),
813                        MediaType.APPLICATION_JSON)),
814                //
815                translateMetadata(meta.getMetadata(), bundle));
816    }
817
818    private String normalizeCategory(final String category) {
819        // we prevent root categories and always append the family in this case
820        if (!category.contains("${family}")) {
821            return category + "/${family}";
822        }
823        return category;
824    }
825
826    private Map<String, String> translateMetadata(final Map<String, String> source, final ComponentBundle bundle) {
827        return source
828                .entrySet()
829                .stream()
830                .map(e -> bundle.displayName(e.getKey().replaceAll("::", "."))
831                        .map(it -> (Entry<String, String>) new SimpleEntry(e.getKey(), it))
832                        .orElse(e))
833                .map(t -> {
834                    if (t.getKey().equals("documentation::value")) {
835                        final String bundleDoc = bundle.documentation().orElse(null);
836                        if (bundleDoc != null) {
837                            t.setValue(bundleDoc);
838                        }
839                    }
840                    return t;
841                })
842                .collect(toMap(Entry::getKey, Entry::getValue));
843    }
844
845    private StreamingOutput onMissingJar(final String id) {
846        throw new WebApplicationException(Response
847                .status(Response.Status.NOT_FOUND)
848                .type(APPLICATION_JSON_TYPE)
849                .entity(new ErrorPayload(PLUGIN_MISSING, "No file found for: " + id))
850                .build());
851    }
852}