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.emptyMap; 019import static java.util.Collections.emptySet; 020import static java.util.Collections.unmodifiableSet; 021import static java.util.Locale.ROOT; 022import static java.util.Optional.ofNullable; 023import static java.util.function.Function.identity; 024import static java.util.stream.Collectors.joining; 025import static java.util.stream.Collectors.toList; 026import static java.util.stream.Collectors.toMap; 027import static lombok.AccessLevel.PACKAGE; 028 029import java.io.BufferedInputStream; 030import java.io.BufferedOutputStream; 031import java.io.BufferedReader; 032import java.io.ByteArrayInputStream; 033import java.io.ByteArrayOutputStream; 034import java.io.IOException; 035import java.io.InputStream; 036import java.io.Reader; 037import java.io.StringReader; 038import java.nio.file.Files; 039import java.nio.file.Path; 040import java.nio.file.StandardCopyOption; 041import java.nio.file.StandardOpenOption; 042import java.util.Collection; 043import java.util.HashMap; 044import java.util.Map; 045import java.util.Properties; 046import java.util.Set; 047import java.util.concurrent.ConcurrentHashMap; 048import java.util.function.Supplier; 049import java.util.jar.Attributes; 050import java.util.jar.JarEntry; 051import java.util.jar.JarOutputStream; 052import java.util.jar.Manifest; 053import java.util.stream.Stream; 054 055import javax.annotation.PostConstruct; 056import javax.enterprise.context.ApplicationScoped; 057import javax.enterprise.inject.spi.CDI; 058import javax.inject.Inject; 059 060import org.talend.sdk.component.api.service.configuration.LocalConfiguration; 061import org.talend.sdk.component.classloader.ConfigurableClassLoader; 062import org.talend.sdk.component.container.Container; 063import org.talend.sdk.component.dependencies.maven.Artifact; 064import org.talend.sdk.component.path.PathFactory; 065import org.talend.sdk.component.runtime.manager.ComponentManager; 066import org.talend.sdk.component.server.configuration.ComponentServerConfiguration; 067 068import lombok.Getter; 069import lombok.RequiredArgsConstructor; 070import lombok.extern.slf4j.Slf4j; 071 072@Slf4j 073@ApplicationScoped 074public class VirtualDependenciesService { 075 076 @Getter(PACKAGE) 077 private final String virtualGroupId = "virtual.talend.component.server.generated."; 078 079 @Getter(PACKAGE) 080 private final String configurationArtifactIdPrefix = "user-local-configuration-"; 081 082 private final Enrichment noCustomization = new Enrichment(false, null, null, null); 083 084 @Inject 085 private ComponentServerConfiguration configuration; 086 087 private final Map<String, Enrichment> enrichmentsPerContainer = new HashMap<>(); 088 089 @Getter 090 private final Map<Artifact, Path> artifactMapping = new ConcurrentHashMap<>(); 091 092 private final Map<Artifact, Supplier<InputStream>> configurationArtifactMapping = new ConcurrentHashMap<>(); 093 094 private Path provisioningM2Base; 095 096 @PostConstruct 097 private void init() { 098 final String m2 = configuration.getUserExtensionsAutoM2Provisioning(); 099 switch (m2) { 100 case "skip": 101 provisioningM2Base = null; 102 break; 103 case "auto": 104 provisioningM2Base = findStudioM2(); 105 break; 106 default: 107 provisioningM2Base = PathFactory.get(m2); 108 } 109 log.debug("m2 provisioning base: {}", provisioningM2Base); 110 } 111 112 public void onDeploy(final String pluginId) { 113 if (!configuration.getUserExtensions().isPresent()) { 114 enrichmentsPerContainer.put(pluginId, noCustomization); 115 return; 116 } 117 final Path extensions = PathFactory 118 .get(configuration.getUserExtensions().orElseThrow(IllegalArgumentException::new)) 119 .resolve(pluginId); 120 if (!Files.exists(extensions)) { 121 log.debug("'{}' does not exist so no extension will be added to family '{}'", extensions, pluginId); 122 enrichmentsPerContainer.put(pluginId, noCustomization); 123 return; 124 } 125 126 final Path userConfig = extensions.resolve("user-configuration.properties"); 127 final Map<Artifact, Path> userJars = findJars(extensions, pluginId); 128 final Properties userConfiguration = loadUserConfiguration(pluginId, userConfig, userJars); 129 if (userConfiguration.isEmpty() && userJars.isEmpty()) { 130 log.debug("No customization for container '{}'", pluginId); 131 enrichmentsPerContainer.put(pluginId, noCustomization); 132 return; 133 } 134 135 final Map<String, String> customConfigAsMap = userConfiguration 136 .stringPropertyNames() 137 .stream() 138 .collect(toMap(identity(), userConfiguration::getProperty)); 139 log 140 .debug("Set up customization for container '{}' (has-configuration={}, jars={})", pluginId, 141 !userConfiguration.isEmpty(), userJars); 142 143 if (userConfiguration.isEmpty()) { 144 enrichmentsPerContainer.put(pluginId, new Enrichment(true, customConfigAsMap, null, userJars.keySet())); 145 } else { 146 final byte[] localConfigurationJar = generateConfigurationJar(pluginId, userConfiguration); 147 final Artifact configurationArtifact; 148 try { 149 configurationArtifact = new Artifact(groupIdFor(pluginId), configurationArtifactIdPrefix + pluginId, 150 "jar", "", Long.toString(Files.getLastModifiedTime(userConfig).toMillis()), "compile"); 151 } catch (final IOException e) { 152 throw new IllegalStateException(e); 153 } 154 doProvision(configurationArtifact, () -> new ByteArrayInputStream(localConfigurationJar)); 155 enrichmentsPerContainer 156 .put(pluginId, new Enrichment(true, customConfigAsMap, configurationArtifact, userJars.keySet())); 157 configurationArtifactMapping 158 .put(configurationArtifact, () -> new ByteArrayInputStream(localConfigurationJar)); 159 } 160 userJars.forEach((artifact, file) -> doProvision(artifact, () -> { 161 try { 162 return Files.newInputStream(file, StandardOpenOption.READ); 163 } catch (final IOException e) { 164 throw new IllegalStateException(e); 165 } 166 })); 167 168 artifactMapping.putAll(userJars); 169 } 170 171 public void onUnDeploy(final Container plugin) { 172 final Enrichment enrichment = enrichmentsPerContainer.remove(plugin.getId()); 173 if (enrichment == null || enrichment == noCustomization) { 174 return; 175 } 176 177 if (enrichment.userArtifacts != null) { 178 enrichment.userArtifacts.forEach(artifactMapping::remove); 179 enrichment.userArtifacts.clear(); 180 } 181 if (enrichment.configurationArtifact != null) { 182 configurationArtifactMapping.remove(enrichment.configurationArtifact); 183 } 184 } 185 186 public boolean isVirtual(final String gav) { 187 return gav.startsWith(virtualGroupId); 188 } 189 190 public Enrichment getEnrichmentFor(final String pluginId) { 191 return enrichmentsPerContainer.get(pluginId); 192 } 193 194 public Stream<Artifact> userArtifactsFor(final String pluginId) { 195 final Enrichment enrichment = enrichmentsPerContainer.get(pluginId); 196 if (enrichment == null || !enrichment.customized) { 197 return Stream.empty(); 198 } 199 final Stream<Artifact> userJars = enrichment.userArtifacts.stream(); 200 if (enrichment.configurationArtifact != null) { 201 // config is added but will be ignored cause not physically here 202 // however it ensures our rest service returns right data 203 return Stream.concat(userJars, Stream.of(enrichment.configurationArtifact)); 204 } 205 return userJars; 206 } 207 208 public Supplier<InputStream> retrieveArtifact(final Artifact artifact) { 209 return ofNullable(artifactMapping.get(artifact)).map(it -> (Supplier<InputStream>) () -> { 210 try { 211 return Files.newInputStream(it); 212 } catch (final IOException e) { 213 throw new IllegalStateException(e); 214 } 215 }).orElseGet(() -> configurationArtifactMapping.get(artifact)); 216 } 217 218 public String groupIdFor(final String family) { 219 return virtualGroupId + sanitizedForGav(family); 220 } 221 222 private byte[] generateConfigurationJar(final String family, final Properties userConfiguration) { 223 final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 224 final Manifest manifest = new Manifest(); 225 final Attributes mainAttributes = manifest.getMainAttributes(); 226 mainAttributes.put(Attributes.Name.MANIFEST_VERSION, "1.0"); 227 mainAttributes.putValue("Created-By", "Talend Component Kit Server"); 228 mainAttributes.putValue("Talend-Time", Long.toString(System.currentTimeMillis())); 229 mainAttributes.putValue("Talend-Family-Name", family); 230 try (final JarOutputStream jar = new JarOutputStream(new BufferedOutputStream(outputStream), manifest)) { 231 jar.putNextEntry(new JarEntry("TALEND-INF/local-configuration.properties")); 232 userConfiguration.store(jar, "Configuration of the family " + family); 233 jar.closeEntry(); 234 } catch (final IOException e) { 235 throw new IllegalStateException(e); 236 } 237 return outputStream.toByteArray(); 238 } 239 240 private Properties loadUserConfiguration(final String plugin, final Path userConfig, 241 final Map<Artifact, Path> userJars) { 242 final Properties properties = new Properties(); 243 if (!Files.exists(userConfig)) { 244 return properties; 245 } 246 final String content; 247 try (final BufferedReader stream = Files.newBufferedReader(userConfig)) { 248 content = stream.lines().collect(joining("\n")); 249 } catch (final IOException e) { 250 throw new IllegalStateException(e); 251 } 252 try (final Reader reader = new StringReader(replaceByGav(plugin, content, userJars))) { 253 properties.load(reader); 254 } catch (final IOException e) { 255 throw new IllegalStateException(e); 256 } 257 return properties; 258 } 259 260 // handle "userJar(name)" function in the config, normally not needed since jars are in the context already 261 // note: if we make it more complex, switch to a real parser or StrSubstitutor 262 String replaceByGav(final String plugin, final String content, final Map<Artifact, Path> userJars) { 263 final StringBuilder output = new StringBuilder(); 264 final String prefixFn = "userJar("; 265 int fnIdx = content.indexOf(prefixFn); 266 int previousEnd = 0; 267 if (fnIdx < 0) { 268 output.append(content); 269 } else { 270 while (fnIdx >= 0) { 271 final int end = content.indexOf(')', fnIdx); 272 output.append(content, previousEnd, fnIdx); 273 output.append(toGav(plugin, content.substring(fnIdx + prefixFn.length(), end), userJars)); 274 fnIdx = content.indexOf(prefixFn, end); 275 if (fnIdx < 0) { 276 if (end < content.length() - 1) { 277 output.append(content, end + 1, content.length()); 278 } 279 } else { 280 previousEnd = end + 1; 281 } 282 } 283 } 284 return output.toString(); 285 } 286 287 private String toGav(final String plugin, final String jarNameWithoutExtension, 288 final Map<Artifact, Path> userJars) { 289 return groupIdFor(plugin) + ':' + jarNameWithoutExtension + ":jar:" 290 + userJars 291 .keySet() 292 .stream() 293 .filter(it -> it.getArtifact().equals(jarNameWithoutExtension)) 294 .findFirst() 295 .map(Artifact::getVersion) 296 .orElse("unknown"); 297 } 298 299 private Map<Artifact, Path> findJars(final Path familyFolder, final String family) { 300 if (!Files.isDirectory(familyFolder)) { 301 return emptyMap(); 302 } 303 try { 304 return Files 305 .list(familyFolder) 306 .filter(file -> file.getFileName().toString().endsWith(".jar")) 307 .collect(toMap(it -> { 308 try { 309 return new Artifact(groupIdFor(family), toArtifact(it), "jar", "", 310 Long.toString(Files.getLastModifiedTime(it).toMillis()), "compile"); 311 } catch (final IOException e) { 312 throw new IllegalStateException(e); 313 } 314 }, identity())); 315 } catch (final IOException e) { 316 throw new IllegalStateException(e); 317 } 318 } 319 320 private String toArtifact(final Path file) { 321 final String name = file.getFileName().toString(); 322 return name.substring(0, name.length() - ".jar".length()); 323 } 324 325 private String sanitizedForGav(final String name) { 326 return name.replace(' ', '_').toLowerCase(ROOT); 327 } 328 329 // note: we don't want to provision based on our real m2, only studio one for now 330 private Path findStudioM2() { 331 if (System.getProperty("talend.studio.version") != null && System.getProperty("osgi.bundles") != null) { 332 final Path localM2 = PathFactory.get(System.getProperty("talend.component.server.maven.repository", "")); 333 if (Files.isDirectory(localM2)) { 334 return localM2; 335 } 336 } 337 return null; 338 } 339 340 private void doProvision(final Artifact artifact, final Supplier<InputStream> newInputStream) { 341 if (provisioningM2Base == null) { 342 log.debug("No m2 to provision, skipping {}", artifact); 343 return; 344 } 345 final Path target = provisioningM2Base.resolve(artifact.toPath()); 346 if (target.toFile().exists()) { 347 log.debug("{} already exists, skipping", target); 348 return; 349 } 350 final Path parentFile = target.getParent(); 351 if (!Files.exists(parentFile)) { 352 try { 353 Files.createDirectories(parentFile); 354 } catch (final IOException e) { 355 throw new IllegalArgumentException("Can't create " + parentFile, e); 356 } 357 } 358 try (final InputStream stream = new BufferedInputStream(newInputStream.get())) { 359 Files.copy(stream, target, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES); 360 } catch (final IOException e) { 361 throw new IllegalStateException(e); 362 } 363 } 364 365 @RequiredArgsConstructor 366 private static class Enrichment { 367 368 private final boolean customized; 369 370 private final Map<String, String> customConfiguration; 371 372 private final Artifact configurationArtifact; 373 374 private final Collection<Artifact> userArtifacts; 375 } 376 377 public static class LocalConfigurationImpl implements LocalConfiguration { 378 379 private final VirtualDependenciesService delegate; 380 381 public LocalConfigurationImpl() { 382 delegate = CDI.current().select(VirtualDependenciesService.class).get(); 383 } 384 385 @Override 386 public String get(final String key) { 387 if (key == null || !key.contains(".")) { 388 return null; 389 } 390 final String plugin = key.substring(0, key.indexOf('.')); 391 final Enrichment enrichment = delegate.getEnrichmentFor(plugin); 392 if (enrichment == null || enrichment.customConfiguration == null) { 393 return null; 394 } 395 return enrichment.customConfiguration.get(key.substring(plugin.length() + 1)); 396 } 397 398 @Override 399 public Set<String> keys() { 400 final ClassLoader loader = Thread.currentThread().getContextClassLoader(); 401 if (!ConfigurableClassLoader.class.isInstance(loader)) { 402 return emptySet(); 403 } 404 final String id = ConfigurableClassLoader.class.cast(loader).getId(); 405 final Enrichment enrichment = delegate.getEnrichmentFor(id); 406 if (enrichment == null || enrichment.customConfiguration == null) { 407 return emptySet(); 408 } 409 return unmodifiableSet(enrichment.customConfiguration.keySet()); 410 } 411 } 412 413 public static class UserContainerClasspathContributor implements ComponentManager.ContainerClasspathContributor { 414 415 private final VirtualDependenciesService delegate; 416 417 public UserContainerClasspathContributor() { 418 delegate = CDI.current().select(VirtualDependenciesService.class).get(); 419 } 420 421 @Override 422 public Collection<Artifact> findContributions(final String pluginId) { 423 delegate.onDeploy(pluginId); 424 return delegate.userArtifactsFor(pluginId).collect(toList()); 425 } 426 427 @Override 428 public boolean canResolve(final String path) { 429 return delegate.isVirtual(path.replace('/', '.')); 430 } 431 432 @Override 433 public Path resolve(final String path) { 434 if (path.contains('/' + delegate.getConfigurationArtifactIdPrefix())) { 435 return null; // not needed, will be enriched on the fly, see LocalConfigurationImpl 436 } 437 final String[] segments = path.split("/"); 438 if (segments.length < 9) { 439 return null; 440 } 441 // ex: virtual.talend.component.server.generated.<plugin id>:<artifact>:jar:dynamic 442 final String group = delegate 443 .groupIdFor(Stream.of(segments).skip(5).limit(segments.length - 5 - 3).collect(joining("."))); 444 final String artifact = segments[segments.length - 3]; 445 return delegate 446 .getArtifactMapping() 447 .entrySet() 448 .stream() 449 .filter(it -> it.getKey().getGroup().equals(group) && it.getKey().getArtifact().equals(artifact)) 450 .findFirst() 451 .map(Map.Entry::getValue) 452 .orElse(null); 453 } 454 } 455}