001/**
002 * Copyright (C) 2006-2020 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.service;
017
018import static java.util.Collections.emptyList;
019import static java.util.Optional.ofNullable;
020import static java.util.stream.Collectors.toList;
021import static java.util.stream.Collectors.toSet;
022import static java.util.stream.Stream.empty;
023
024import java.io.IOException;
025import java.io.InputStream;
026import java.nio.file.Files;
027import java.nio.file.Path;
028import java.util.Collection;
029import java.util.Date;
030import java.util.List;
031import java.util.Locale;
032import java.util.Properties;
033import java.util.function.Supplier;
034import java.util.stream.Stream;
035
036import javax.annotation.PostConstruct;
037import javax.annotation.PreDestroy;
038import javax.enterprise.context.ApplicationScoped;
039import javax.enterprise.context.Initialized;
040import javax.enterprise.event.Event;
041import javax.enterprise.event.Observes;
042import javax.enterprise.inject.Produces;
043import javax.inject.Inject;
044import javax.ws.rs.core.Context;
045import javax.ws.rs.core.UriInfo;
046
047import org.talend.sdk.component.container.Container;
048import org.talend.sdk.component.container.ContainerListener;
049import org.talend.sdk.component.dependencies.maven.Artifact;
050import org.talend.sdk.component.dependencies.maven.MvnCoordinateToFileConverter;
051import org.talend.sdk.component.design.extension.RepositoryModel;
052import org.talend.sdk.component.design.extension.repository.Config;
053import org.talend.sdk.component.path.PathFactory;
054import org.talend.sdk.component.runtime.manager.ComponentFamilyMeta;
055import org.talend.sdk.component.runtime.manager.ComponentManager;
056import org.talend.sdk.component.runtime.manager.ContainerComponentRegistry;
057import org.talend.sdk.component.server.configuration.ComponentServerConfiguration;
058import org.talend.sdk.component.server.dao.ComponentActionDao;
059import org.talend.sdk.component.server.dao.ComponentDao;
060import org.talend.sdk.component.server.dao.ComponentFamilyDao;
061import org.talend.sdk.component.server.dao.ConfigurationDao;
062import org.talend.sdk.component.server.service.event.DeployedComponent;
063
064import lombok.AllArgsConstructor;
065import lombok.Data;
066import lombok.extern.slf4j.Slf4j;
067
068@Slf4j
069@ApplicationScoped
070public class ComponentManagerService {
071
072    @Inject
073    private ComponentServerConfiguration configuration;
074
075    @Inject
076    private ComponentDao componentDao;
077
078    @Inject
079    private ComponentFamilyDao componentFamilyDao;
080
081    @Inject
082    private ComponentActionDao actionDao;
083
084    @Inject
085    private ConfigurationDao configurationDao;
086
087    @Inject
088    private VirtualDependenciesService virtualDependenciesService;
089
090    @Inject
091    private GlobService globService;
092
093    @Inject
094    private Event<DeployedComponent> deployedComponentEvent;
095
096    @Inject
097    @Context
098    private UriInfo uriInfo;
099
100    @Inject
101    private LocaleMapper localeMapper;
102
103    private ComponentManager instance;
104
105    private MvnCoordinateToFileConverter mvnCoordinateToFileConverter;
106
107    private DeploymentListener deploymentListener;
108
109    private volatile Date lastUpdated = new Date();
110
111    private boolean started;
112
113    public void startupLoad(@Observes @Initialized(ApplicationScoped.class) final Object start) {
114        // no-op
115    }
116
117    @PostConstruct
118    private void init() {
119        if (log.isWarnEnabled()) {
120            final String filter = System.getProperty("jdk.serialFilter");
121            if (filter == null) {
122                log.warn("No system property 'jdk.serialFilter', ensure it is intended");
123            }
124        }
125
126        mvnCoordinateToFileConverter = new MvnCoordinateToFileConverter();
127        final Path m2 = configuration
128                .getMavenRepository()
129                .map(PathFactory::get)
130                .filter(Files::exists)
131                .orElseGet(ComponentManager::findM2);
132        log.info("Using maven repository: '{}'", m2);
133        instance = new ComponentManager(m2) {
134
135            @Override
136            protected Supplier<Locale> getLocalSupplier() {
137                return ComponentManagerService.this::readCurrentLocale;
138            }
139        };
140        deploymentListener = new DeploymentListener(componentDao, componentFamilyDao, actionDao, configurationDao,
141                virtualDependenciesService);
142        instance.getContainer().registerListener(deploymentListener);
143
144        // note: we don't want to download anything from the manager, if we need to download any artifact we need
145        // to ensure it is controlled (secured) and allowed so don't make it implicit but enforce a first phase
146        // where it is cached locally (provisioning solution)
147        final List<String> coords = configuration
148                .getComponentCoordinates()
149                .map(it -> Stream.of(it.split(",")).map(String::trim).filter(i -> !i.isEmpty()).collect(toList()))
150                .orElse(emptyList());
151        coords.forEach(this::deploy);
152        configuration
153                .getComponentRegistry()
154                .map(Collection::stream)
155                .orElseGet(Stream::empty)
156                .flatMap(globService::toFiles)
157                .forEach(registry -> {
158                    final Properties properties = new Properties();
159                    try (final InputStream is = Files.newInputStream(registry)) {
160                        properties.load(is);
161                    } catch (final IOException e) {
162                        throw new IllegalArgumentException(e);
163                    }
164                    properties
165                            .stringPropertyNames()
166                            .stream()
167                            .map(properties::getProperty)
168                            .filter(gav -> !coords.contains(gav))
169                            .forEach(this::deploy);
170                });
171        started = true;
172    }
173
174    private Locale readCurrentLocale() {
175        try {
176            return ofNullable(uriInfo.getQueryParameters().getFirst("lang"))
177                    .map(localeMapper::mapLocale)
178                    .orElseGet(Locale::getDefault);
179        } catch (final RuntimeException ex) {
180            log.debug("Can't get the locale from current request in thread '{}'", Thread.currentThread().getName(), ex);
181            return Locale.getDefault();
182        }
183    }
184
185    @PreDestroy
186    private void destroy() {
187        started = false;
188        instance.getContainer().unregisterListener(deploymentListener);
189        instance.close();
190    }
191
192    public String deploy(final String pluginGAV) {
193        final String pluginPath = ofNullable(pluginGAV)
194                .map(gav -> mvnCoordinateToFileConverter.toArtifact(gav))
195                .map(Artifact::toPath)
196                .orElseThrow(() -> new IllegalArgumentException("Plugin GAV can't be empty"));
197
198        final Path m2 = instance.getContainer().getRootRepositoryLocationPath();
199        final String plugin =
200                instance.addWithLocationPlugin(pluginGAV, m2.resolve(pluginPath).toAbsolutePath().toString());
201        lastUpdated = new Date();
202        if (started) {
203            deployedComponentEvent.fire(new DeployedComponent());
204        }
205        return plugin;
206    }
207
208    public void undeploy(final String pluginGAV) {
209        if (pluginGAV == null || pluginGAV.isEmpty()) {
210            throw new IllegalArgumentException("plugin maven GAV are required to undeploy a plugin");
211        }
212
213        String pluginID = instance
214                .find(c -> pluginGAV.equals(c.get(ComponentManager.OriginalId.class).getValue()) ? Stream.of(c.getId())
215                        : empty())
216                .findFirst()
217                .orElseThrow(() -> new IllegalArgumentException("No plugin found using maven GAV: " + pluginGAV));
218
219        instance.removePlugin(pluginID);
220        lastUpdated = new Date();
221    }
222
223    public Date findLastUpdated() {
224        return lastUpdated;
225    }
226
227    @AllArgsConstructor
228    private static class DeploymentListener implements ContainerListener {
229
230        private final ComponentDao componentDao;
231
232        private final ComponentFamilyDao componentFamilyDao;
233
234        private final ComponentActionDao actionDao;
235
236        private final ConfigurationDao configurationDao;
237
238        private final VirtualDependenciesService virtualDependenciesService;
239
240        @Override
241        public void onCreate(final Container container) {
242            container.set(CleanupTask.class, new CleanupTask(postDeploy(container)));
243        }
244
245        @Override
246        public void onClose(final Container container) {
247            if (container.getState() == Container.State.ON_ERROR) {
248                // means it was not deployed so don't drop old state
249                return;
250            }
251            ofNullable(container.get(CleanupTask.class)).ifPresent(c -> c.getCleanup().run());
252        }
253
254        private Runnable postDeploy(final Container plugin) {
255            final Collection<String> componentIds = plugin
256                    .get(ContainerComponentRegistry.class)
257                    .getComponents()
258                    .values()
259                    .stream()
260                    .flatMap(c -> Stream
261                            .concat(c.getPartitionMappers().values().stream(), c.getProcessors().values().stream()))
262                    .peek(componentDao::createOrUpdate)
263                    .map(ComponentFamilyMeta.BaseMeta::getId)
264                    .collect(toSet());
265
266            final Collection<ComponentActionDao.ActionKey> actions = plugin
267                    .get(ContainerComponentRegistry.class)
268                    .getServices()
269                    .stream()
270                    .flatMap(c -> c.getActions().stream())
271                    .map(actionDao::createOrUpdate)
272                    .collect(toList());
273
274            final Collection<String> families = plugin
275                    .get(ContainerComponentRegistry.class)
276                    .getComponents()
277                    .values()
278                    .stream()
279                    .map(componentFamilyDao::createOrUpdate)
280                    .collect(toList());
281
282            final Collection<String> configs = ofNullable(plugin.get(RepositoryModel.class))
283                    .map(r -> r
284                            .getFamilies()
285                            .stream()
286                            .flatMap(f -> configAsStream(f.getConfigs().get().stream()))
287                            .collect(toList()))
288                    .orElse(emptyList())
289                    .stream()
290                    .map(configurationDao::createOrUpdate)
291                    .collect(toList());
292
293            return () -> {
294                virtualDependenciesService.onUnDeploy(plugin);
295                componentIds.forEach(componentDao::removeById);
296                actions.forEach(actionDao::removeById);
297                families.forEach(componentFamilyDao::removeById);
298                configs.forEach(configurationDao::removeById);
299            };
300        }
301
302        private Stream<Config> configAsStream(final Stream<Config> stream) {
303            return stream.flatMap(s -> Stream.concat(Stream.of(s), s.getChildConfigs().stream()));
304        }
305    }
306
307    @Data
308    private static class CleanupTask {
309
310        private final Runnable cleanup;
311    }
312
313    @Produces
314    public ComponentManager manager() {
315        return instance;
316    }
317
318}