All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.talend.sdk.component.server.front.ComponentResourceImpl Maven / Gradle / Ivy

There is a newer version: 10.57.0
Show newest version
/**
 * Copyright (C) 2006-2020 Talend Inc. - www.talend.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.talend.sdk.component.server.front;

import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
import static java.util.Optional.ofNullable;
import static java.util.stream.Collectors.toList;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE;
import static org.talend.sdk.component.server.front.model.ErrorDictionary.COMPONENT_MISSING;
import static org.talend.sdk.component.server.front.model.ErrorDictionary.DESIGN_MODEL_MISSING;
import static org.talend.sdk.component.server.front.model.ErrorDictionary.PLUGIN_MISSING;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Stream;

import javax.annotation.PostConstruct;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Observes;
import javax.inject.Inject;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.StreamingOutput;

import org.talend.sdk.component.container.Container;
import org.talend.sdk.component.dependencies.maven.Artifact;
import org.talend.sdk.component.design.extension.DesignModel;
import org.talend.sdk.component.runtime.manager.ComponentFamilyMeta;
import org.talend.sdk.component.runtime.manager.ComponentManager;
import org.talend.sdk.component.runtime.manager.ContainerComponentRegistry;
import org.talend.sdk.component.runtime.manager.extension.ComponentContexts;
import org.talend.sdk.component.server.api.ComponentResource;
import org.talend.sdk.component.server.configuration.ComponentServerConfiguration;
import org.talend.sdk.component.server.dao.ComponentDao;
import org.talend.sdk.component.server.dao.ComponentFamilyDao;
import org.talend.sdk.component.server.front.base.internal.RequestKey;
import org.talend.sdk.component.server.front.model.ComponentDetail;
import org.talend.sdk.component.server.front.model.ComponentDetailList;
import org.talend.sdk.component.server.front.model.ComponentId;
import org.talend.sdk.component.server.front.model.ComponentIndex;
import org.talend.sdk.component.server.front.model.ComponentIndices;
import org.talend.sdk.component.server.front.model.Dependencies;
import org.talend.sdk.component.server.front.model.DependencyDefinition;
import org.talend.sdk.component.server.front.model.ErrorDictionary;
import org.talend.sdk.component.server.front.model.Icon;
import org.talend.sdk.component.server.front.model.Link;
import org.talend.sdk.component.server.front.model.SimplePropertyDefinition;
import org.talend.sdk.component.server.front.model.error.ErrorPayload;
import org.talend.sdk.component.server.lang.MapCache;
import org.talend.sdk.component.server.service.ActionsService;
import org.talend.sdk.component.server.service.ComponentManagerService;
import org.talend.sdk.component.server.service.ExtensionComponentMetadataManager;
import org.talend.sdk.component.server.service.IconResolver;
import org.talend.sdk.component.server.service.LocaleMapper;
import org.talend.sdk.component.server.service.PropertiesService;
import org.talend.sdk.component.server.service.SimpleQueryLanguageCompiler;
import org.talend.sdk.component.server.service.VirtualDependenciesService;
import org.talend.sdk.component.server.service.event.DeployedComponent;
import org.talend.sdk.component.spi.component.ComponentExtension;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@ApplicationScoped
public class ComponentResourceImpl implements ComponentResource {

    private final ConcurrentMap indicesPerRequest = new ConcurrentHashMap<>();

    @Inject
    private ComponentManager manager;

    @Inject
    private ComponentManagerService componentManagerService;

    @Inject
    private ComponentDao componentDao;

    @Inject
    private ComponentFamilyDao componentFamilyDao;

    @Inject
    private LocaleMapper localeMapper;

    @Inject
    private ActionsService actionsService;

    @Inject
    private PropertiesService propertiesService;

    @Inject
    private IconResolver iconResolver;

    @Inject
    private ComponentServerConfiguration configuration;

    @Inject
    private VirtualDependenciesService virtualDependenciesService;

    @Inject
    private ExtensionComponentMetadataManager virtualComponents;

    @Inject
    private MapCache caches;

    @Inject
    private SimpleQueryLanguageCompiler queryLanguageCompiler;

    private Map> componentEvaluators = new HashMap<>();

    @PostConstruct
    private void setupRuntime() {
        log.info("Initializing " + getClass());

        // preload some highly used data
        getIndex("en", false, null);

        componentEvaluators.put("plugin", c -> c.getId().getPlugin());
        componentEvaluators.put("id", c -> c.getId().getId());
        componentEvaluators.put("familyId", c -> c.getId().getFamilyId());
        componentEvaluators.put("name", c -> c.getId().getName());
        componentEvaluators.put("metadata", component -> {
            final Iterator iterator =
                    getDetail("en", new String[] { component.getId().getId() })
                            .getDetails()
                            .iterator()
                            .next()
                            .getProperties()
                            .iterator();
            if (iterator.hasNext()) {
                return iterator.next().getMetadata();
            }
            return Collections.emptyMap();
        });
    }

    public void clearCache(@Observes final DeployedComponent deployedComponent) {
        indicesPerRequest.clear();
    }

    @Override
    public Dependencies getDependencies(final String[] ids) {
        if (ids.length == 0) {
            return new Dependencies(emptyMap());
        }
        final Map dependencies = new HashMap<>();
        for (final String id : ids) {
            if (virtualComponents.isExtensionEntity(id)) {
                final DependencyDefinition deps = ofNullable(virtualComponents.getDependenciesFor(id))
                        .orElseGet(() -> new DependencyDefinition(emptyList()));
                dependencies.put(id, deps);
            } else {
                final ComponentFamilyMeta.BaseMeta meta = componentDao.findById(id);
                dependencies.put(meta.getId(), getDependenciesFor(meta));
            }
        }
        return new Dependencies(dependencies);
    }

    @Override
    public StreamingOutput getDependency(final String id) {
        final ComponentFamilyMeta.BaseMeta component = componentDao.findById(id);
        final Supplier streamProvider;
        if (component != null) { // local dep
            final Path file = componentManagerService
                    .manager()
                    .findPlugin(component.getParent().getPlugin())
                    .orElseThrow(() -> new WebApplicationException(Response
                            .status(Response.Status.NOT_FOUND)
                            .type(APPLICATION_JSON_TYPE)
                            .entity(new ErrorPayload(PLUGIN_MISSING, "No plugin matching the id: " + id))
                            .build()))
                    .getContainerFile()
                    .orElseThrow(() -> new WebApplicationException(Response
                            .status(Response.Status.NOT_FOUND)
                            .type(APPLICATION_JSON_TYPE)
                            .entity(new ErrorPayload(PLUGIN_MISSING, "No dependency matching the id: " + id))
                            .build()));
            if (!Files.exists(file)) {
                return onMissingJar(id);
            }
            streamProvider = () -> {
                try {
                    return Files.newInputStream(file);
                } catch (final IOException e) {
                    throw new IllegalStateException(e);
                }
            };
        } else { // just try to resolve it locally, note we would need to ensure some security here
            final Artifact artifact = Artifact.from(id);
            if (virtualDependenciesService.isVirtual(id)) {
                streamProvider = virtualDependenciesService.retrieveArtifact(artifact);
                if (streamProvider == null) {
                    return onMissingJar(id);
                }
            } else {
                final Path file = componentManagerService.manager().getContainer().resolve(artifact.toPath());
                if (!Files.exists(file)) {
                    return onMissingJar(id);
                }
                streamProvider = () -> {
                    try {
                        return Files.newInputStream(file);
                    } catch (final IOException e) {
                        throw new IllegalStateException(e);
                    }
                };
            }
        }
        return output -> {
            final byte[] buffer = new byte[40960]; // 5k
            try (final InputStream stream = new BufferedInputStream(streamProvider.get(), buffer.length)) {
                int count;
                while ((count = stream.read(buffer)) >= 0) {
                    if (count == 0) {
                        continue;
                    }
                    output.write(buffer, 0, count);
                }
            }
        };
    }

    @Override
    public ComponentIndices getIndex(final String language, final boolean includeIconContent, final String query) {
        final Locale locale = localeMapper.mapLocale(language);
        caches.evictIfNeeded(indicesPerRequest, configuration.getMaxCacheSize() - 1);
        return indicesPerRequest.computeIfAbsent(new RequestKey(locale, includeIconContent, query), k -> {
            final Predicate filter = queryLanguageCompiler.compile(query, componentEvaluators);
            return new ComponentIndices(Stream
                    .concat(findDeployedComponents(includeIconContent, locale),
                            virtualComponents
                                    .getDetails()
                                    .stream()
                                    .map(detail -> new ComponentIndex(detail.getId(), detail.getDisplayName(),
                                            detail.getId().getFamily(), new Icon(detail.getIcon(), null, null),
                                            new Icon(virtualComponents.getFamilyIconFor(detail.getId().getFamilyId()),
                                                    null, null),
                                            detail.getVersion(), singletonList(detail.getId().getFamily()),
                                            detail.getLinks())))
                    .filter(filter)
                    .collect(toList()));
        });
    }

    @Override
    public Response familyIcon(final String id) {
        if (virtualComponents.isExtensionEntity(id)) { // todo or just use front bundle?
            return Response
                    .status(Response.Status.NOT_FOUND)
                    .entity(new ErrorPayload(ErrorDictionary.ICON_MISSING, "No icon for family: " + id))
                    .type(APPLICATION_JSON_TYPE)
                    .build();
        }

        // todo: add caching if SvgIconResolver becomes used a lot - not the case ATM
        final ComponentFamilyMeta meta = componentFamilyDao.findById(id);
        if (meta == null) {
            return Response
                    .status(Response.Status.NOT_FOUND)
                    .entity(new ErrorPayload(ErrorDictionary.FAMILY_MISSING, "No family for identifier: " + id))
                    .type(APPLICATION_JSON_TYPE)
                    .build();
        }
        final Optional plugin = manager.findPlugin(meta.getPlugin());
        if (!plugin.isPresent()) {
            return Response
                    .status(Response.Status.NOT_FOUND)
                    .entity(new ErrorPayload(ErrorDictionary.PLUGIN_MISSING,
                            "No plugin '" + meta.getPlugin() + "' for identifier: " + id))
                    .type(APPLICATION_JSON_TYPE)
                    .build();
        }

        final IconResolver.Icon iconContent = iconResolver.resolve(plugin.get(), meta.getIcon());
        if (iconContent == null) {
            return Response
                    .status(Response.Status.NOT_FOUND)
                    .entity(new ErrorPayload(ErrorDictionary.ICON_MISSING, "No icon for family identifier: " + id))
                    .type(APPLICATION_JSON_TYPE)
                    .build();
        }

        return Response.ok(iconContent.getBytes()).type(iconContent.getType()).build();
    }

    @Override
    public Response icon(final String id) {
        if (virtualComponents.isExtensionEntity(id)) { // todo if the front bundle is not sufficient
            return Response
                    .status(Response.Status.NOT_FOUND)
                    .entity(new ErrorPayload(ErrorDictionary.ICON_MISSING, "No icon for family: " + id))
                    .type(APPLICATION_JSON_TYPE)
                    .build();
        }

        // todo: add caching if SvgIconResolver becomes used a lot - not the case ATM
        final ComponentFamilyMeta.BaseMeta meta = componentDao.findById(id);
        if (meta == null) {
            return Response
                    .status(Response.Status.NOT_FOUND)
                    .entity(new ErrorPayload(ErrorDictionary.COMPONENT_MISSING, "No component for identifier: " + id))
                    .type(APPLICATION_JSON_TYPE)
                    .build();
        }

        final Optional plugin = manager.findPlugin(meta.getParent().getPlugin());
        if (!plugin.isPresent()) {
            return Response
                    .status(Response.Status.NOT_FOUND)
                    .entity(new ErrorPayload(ErrorDictionary.PLUGIN_MISSING,
                            "No plugin '" + meta.getParent().getPlugin() + "' for identifier: " + id))
                    .type(APPLICATION_JSON_TYPE)
                    .build();
        }

        final IconResolver.Icon iconContent = iconResolver.resolve(plugin.get(), meta.getIcon());
        if (iconContent == null) {
            return Response
                    .status(Response.Status.NOT_FOUND)
                    .entity(new ErrorPayload(ErrorDictionary.ICON_MISSING, "No icon for identifier: " + id))
                    .type(APPLICATION_JSON_TYPE)
                    .build();
        }

        return Response.ok(iconContent.getBytes()).type(iconContent.getType()).build();
    }

    @Override
    public Map migrate(final String id, final int version, final Map config) {
        if (virtualComponents.isExtensionEntity(id)) {
            return config;
        }
        return ofNullable(componentDao.findById(id))
                .orElseThrow(() -> new WebApplicationException(Response
                        .status(Response.Status.NOT_FOUND)
                        .entity(new ErrorPayload(ErrorDictionary.COMPONENT_MISSING, "Didn't find component " + id))
                        .build()))
                .getMigrationHandler()
                .get()
                .migrate(version, config);
    }

    @Override // TODO: max ids.length
    public ComponentDetailList getDetail(final String language, final String[] ids) {
        if (ids == null || ids.length == 0) {
            return new ComponentDetailList(emptyList());
        }

        final Map errors = new HashMap<>();
        final List details = Stream.of(ids).map(id -> {
            if (virtualComponents.isExtensionEntity(id)) {
                return virtualComponents.findComponentById(id).orElseGet(() -> {
                    errors.put(id, new ErrorPayload(COMPONENT_MISSING, "No virtual component '" + id + "'"));
                    return null;
                });
            }
            return ofNullable(componentDao.findById(id)).map(meta -> {
                final Optional plugin = manager.findPlugin(meta.getParent().getPlugin());
                if (!plugin.isPresent()) {
                    errors
                            .put(meta.getId(), new ErrorPayload(PLUGIN_MISSING,
                                    "No plugin '" + meta.getParent().getPlugin() + "'"));
                    return null;
                }

                final Container container = plugin.get();
                final Optional model = ofNullable(meta.get(DesignModel.class));
                if (!model.isPresent()) {
                    errors
                            .put(meta.getId(),
                                    new ErrorPayload(DESIGN_MODEL_MISSING, "No design model '" + meta.getId() + "'"));
                    return null;
                }

                final Locale locale = localeMapper.mapLocale(language);
                final boolean isProcessor = ComponentFamilyMeta.ProcessorMeta.class.isInstance(meta);

                final ComponentDetail componentDetail = new ComponentDetail();
                componentDetail.setLinks(emptyList() /* todo ? */);
                componentDetail.setId(createMetaId(container, meta));
                componentDetail.setVersion(meta.getVersion());
                componentDetail.setIcon(meta.getIcon());
                componentDetail.setInputFlows(model.get().getInputFlows());
                componentDetail.setOutputFlows(model.get().getOutputFlows());
                componentDetail.setType(isProcessor ? "processor" : "input");
                componentDetail
                        .setDisplayName(
                                meta.findBundle(container.getLoader(), locale).displayName().orElse(meta.getName()));
                componentDetail
                        .setProperties(propertiesService
                                .buildProperties(meta.getParameterMetas().get(), container.getLoader(), locale, null)
                                .collect(toList()));
                componentDetail
                        .setActions(actionsService
                                .findActions(meta.getParent().getName(), container, locale, meta,
                                        meta.getParent().findBundle(container.getLoader(), locale)));
                if (isProcessor) {
                    componentDetail.setMetadata(emptyMap());
                } else {
                    componentDetail
                            .setMetadata(singletonMap("mapper::infinite", Boolean
                                    .toString(ComponentFamilyMeta.PartitionMapperMeta.class.cast(meta).isInfinite())));
                }

                return componentDetail;
            }).orElseGet(() -> {
                errors.put(id, new ErrorPayload(COMPONENT_MISSING, "No component '" + id + "'"));
                return null;
            });
        }).filter(Objects::nonNull).collect(toList());

        if (!errors.isEmpty()) {
            throw new WebApplicationException(Response.status(Response.Status.BAD_REQUEST).entity(errors).build());
        }

        return new ComponentDetailList(details);
    }

    private Stream findDeployedComponents(final boolean includeIconContent, final Locale locale) {
        return manager
                .find(c -> c
                        .execute(() -> c.get(ContainerComponentRegistry.class).getComponents().values().stream())
                        .flatMap(component -> Stream
                                .concat(component
                                        .getPartitionMappers()
                                        .values()
                                        .stream()
                                        .map(mapper -> toComponentIndex(c, locale, c.getId(), mapper,
                                                c.get(ComponentManager.OriginalId.class), includeIconContent)),
                                        component
                                                .getProcessors()
                                                .values()
                                                .stream()
                                                .map(proc -> toComponentIndex(c, locale, c.getId(), proc,
                                                        c.get(ComponentManager.OriginalId.class),
                                                        includeIconContent)))));
    }

    private DependencyDefinition getDependenciesFor(final ComponentFamilyMeta.BaseMeta meta) {
        final ComponentFamilyMeta familyMeta = meta.getParent();
        final Optional container = componentManagerService.manager().findPlugin(familyMeta.getPlugin());
        return new DependencyDefinition(container.map(c -> {
            final ComponentExtension.ComponentContext context =
                    c.get(ComponentContexts.class).getContexts().get(meta.getType());
            final ComponentExtension extension = context.owningExtension();
            final Stream deps = c.findDependencies();
            final Stream artifacts;
            if (configuration.getAddExtensionDependencies() && extension != null) {
                final List dependencies = deps.collect(toList());
                final Stream addDeps = getExtensionDependencies(extension, dependencies);
                artifacts = Stream.concat(dependencies.stream(), addDeps);
            } else {
                artifacts = deps;
            }
            return artifacts.map(Artifact::toCoordinate).collect(toList());
        }).orElseThrow(() -> new IllegalArgumentException("Can't find container '" + meta.getId() + "'")));
    }

    private Stream getExtensionDependencies(final ComponentExtension extension,
            final List filtered) {
        return extension
                .getAdditionalDependencies()
                .stream()
                .map(Artifact::from)
                // filter required artifacts if they are already present in the list.
                .filter(extArtifact -> filtered
                        .stream()
                        .map(d -> d.getGroup() + ":" + d.getArtifact())
                        .noneMatch(ga -> ga.equals(extArtifact.getGroup() + ":" + extArtifact.getArtifact())));
    }

    private ComponentId createMetaId(final Container container, final ComponentFamilyMeta.BaseMeta meta) {
        return new ComponentId(meta.getId(), meta.getParent().getId(), meta.getParent().getPlugin(),
                ofNullable(container.get(ComponentManager.OriginalId.class))
                        .map(ComponentManager.OriginalId::getValue)
                        .orElse(container.getId()),
                meta.getParent().getName(), meta.getName());
    }

    private ComponentIndex toComponentIndex(final Container container, final Locale locale, final String plugin,
            final ComponentFamilyMeta.BaseMeta meta, final ComponentManager.OriginalId originalId,
            final boolean includeIcon) {
        final ClassLoader loader = container.getLoader();
        final String icon = meta.getIcon();
        final String familyIcon = meta.getParent().getIcon();
        final IconResolver.Icon iconContent = iconResolver.resolve(container, icon);
        final IconResolver.Icon iconFamilyContent = iconResolver.resolve(container, familyIcon);
        final String familyDisplayName =
                meta.getParent().findBundle(loader, locale).displayName().orElse(meta.getParent().getName());
        final List categories = ofNullable(meta.getParent().getCategories())
                .map(vals -> vals
                        .stream()
                        .map(this::normalizeCategory)
                        .map(category -> category.replace("${family}", meta.getParent().getName())) // not
                                                                                                    // i18n-ed
                                                                                                    // yet
                        .map(category -> meta
                                .getParent()
                                .findBundle(loader, locale)
                                .category(category)
                                .orElseGet(() -> category
                                        .replace("/" + meta.getParent().getName() + "/",
                                                "/" + familyDisplayName + "/")))
                        .collect(toList()))
                .orElseGet(Collections::emptyList);
        return new ComponentIndex(
                new ComponentId(meta.getId(), meta.getParent().getId(), plugin,
                        ofNullable(originalId).map(ComponentManager.OriginalId::getValue).orElse(plugin),
                        meta.getParent().getName(), meta.getName()),
                meta.findBundle(loader, locale).displayName().orElse(meta.getName()), familyDisplayName,
                new Icon(icon, iconContent == null ? null : iconContent.getType(),
                        !includeIcon ? null : (iconContent == null ? null : iconContent.getBytes())),
                new Icon(familyIcon, iconFamilyContent == null ? null : iconFamilyContent.getType(),
                        !includeIcon ? null : (iconFamilyContent == null ? null : iconFamilyContent.getBytes())),
                meta.getVersion(), categories, singletonList(new Link("Detail",
                        "/component/details?identifiers=" + meta.getId(), MediaType.APPLICATION_JSON)));
    }

    private String normalizeCategory(final String category) {
        // we prevent root categories and always append the family in this case
        if (!category.contains("${family}")) {
            return category + "/${family}";
        }
        return category;
    }

    private StreamingOutput onMissingJar(final String id) {
        throw new WebApplicationException(Response
                .status(Response.Status.NOT_FOUND)
                .type(APPLICATION_JSON_TYPE)
                .entity(new ErrorPayload(PLUGIN_MISSING, "No file found for: " + id))
                .build());
    }
}