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}