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