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.singletonMap; 019import static java.util.Optional.ofNullable; 020import static java.util.concurrent.TimeUnit.MILLISECONDS; 021import static java.util.function.Function.identity; 022import static java.util.stream.Collectors.toList; 023import static java.util.stream.Collectors.toMap; 024 025import java.io.BufferedOutputStream; 026import java.io.IOException; 027import java.nio.file.Files; 028import java.nio.file.Path; 029import java.nio.file.StandardOpenOption; 030import java.util.ArrayList; 031import java.util.Collection; 032import java.util.LinkedHashMap; 033import java.util.Map; 034import java.util.Objects; 035import java.util.Optional; 036import java.util.concurrent.ExecutionException; 037import java.util.concurrent.TimeoutException; 038import java.util.function.Consumer; 039import java.util.jar.JarOutputStream; 040import java.util.stream.Stream; 041 042import javax.enterprise.context.ApplicationScoped; 043import javax.enterprise.context.Initialized; 044import javax.enterprise.event.Event; 045import javax.enterprise.event.NotificationOptions; 046import javax.enterprise.event.Observes; 047import javax.inject.Inject; 048import javax.ws.rs.core.MediaType; 049 050import org.talend.sdk.component.path.PathFactory; 051import org.talend.sdk.component.server.configuration.ComponentServerConfiguration; 052import org.talend.sdk.component.server.extension.api.ExtensionRegistrar; 053import org.talend.sdk.component.server.extension.api.action.Action; 054import org.talend.sdk.component.server.front.model.ComponentDetail; 055import org.talend.sdk.component.server.front.model.ComponentId; 056import org.talend.sdk.component.server.front.model.ConfigTypeNode; 057import org.talend.sdk.component.server.front.model.DependencyDefinition; 058import org.talend.sdk.component.server.front.model.Link; 059 060import lombok.extern.slf4j.Slf4j; 061 062@Slf4j 063@ApplicationScoped 064public class ExtensionComponentMetadataManager { 065 066 private static final String EXTENSION_MARKER = "extension::"; 067 068 @Inject 069 private Event<ExtensionRegistrar> extensionsEvent; 070 071 private final Collection<Runnable> waiters = new ArrayList<>(); 072 073 private final Map<String, ComponentDetail> details = new LinkedHashMap<>(); 074 075 private final Map<String, ConfigTypeNode> configurations = new LinkedHashMap<>(); 076 077 private final Map<String, DependencyDefinition> dependencies = new LinkedHashMap<>(); 078 079 private final Map<ActionKey, Action> actions = new LinkedHashMap<>(); 080 081 public void startupLoad(@Observes @Initialized(ApplicationScoped.class) final Object start, 082 final ComponentServerConfiguration configuration) { 083 try { 084 extensionsEvent.fireAsync(new ExtensionRegistrar() { 085 086 @Override 087 public void registerAwait(final Runnable waiter) { 088 synchronized (waiters) { 089 waiters.add(waiter); 090 } 091 } 092 093 @Override 094 public void registerActions(final Collection<Action> userActions) { 095 final Map<ActionKey, Action> actionMap = 096 userActions 097 .stream() 098 .collect(toMap( 099 it -> new ActionKey(it.getReference().getFamily(), 100 it.getReference().getType(), it.getReference().getName()), 101 identity())); 102 synchronized (actions) { 103 actions.putAll(actionMap); 104 } 105 } 106 107 @Override 108 public void registerComponents(final Collection<ComponentDetail> components) { 109 final Map<String, ComponentDetail> mapped = components 110 .stream() 111 .map(it -> new ComponentDetail( 112 new ComponentId(it.getId().getId(), EXTENSION_MARKER + it.getId().getFamilyId(), 113 EXTENSION_MARKER + it.getId().getPlugin(), 114 EXTENSION_MARKER + it.getId().getPluginLocation(), it.getId().getFamily(), 115 it.getId().getName()), 116 it.getDisplayName(), it.getIcon(), it.getType(), it.getVersion(), 117 it.getProperties(), it.getActions(), it.getInputFlows(), it.getOutputFlows(), 118 Stream 119 .concat(createBuiltInLinks(it), 120 it.getLinks() == null ? Stream.empty() : it.getLinks().stream()) 121 .distinct() 122 .collect(toList()), 123 singletonMap("mapper::infinite", "false"))) 124 .collect(toMap(it -> it.getId().getId(), identity(), (a, b) -> { 125 throw new IllegalArgumentException(a + " and " + b + " are conflicting"); 126 }, LinkedHashMap::new)); 127 synchronized (details) { 128 details.putAll(mapped); 129 } 130 } 131 132 @Override 133 public void registerConfigurations(final Collection<ConfigTypeNode> configs) { 134 final Map<String, ConfigTypeNode> mapped = 135 configs.stream().collect(toMap(ConfigTypeNode::getId, identity(), (a, b) -> { 136 throw new IllegalArgumentException(a + " and " + b + " are conflicting"); 137 }, LinkedHashMap::new)); 138 synchronized (configurations) { 139 configurations.putAll(mapped); 140 } 141 } 142 143 @Override 144 public void registerDependencies(final Map<String, DependencyDefinition> deps) { 145 synchronized (dependencies) { 146 dependencies.putAll(deps); 147 } 148 } 149 150 @Override 151 public void createExtensionJarIfNotExist(final String groupId, final String artifactId, 152 final String version, final Consumer<JarOutputStream> creator) { 153 final String m2 = configuration 154 .getExtensionMavenRepository() 155 .orElseThrow( 156 () -> new IllegalArgumentException("No extension maven repository configured")); 157 final Path path = PathFactory.get(m2); 158 final Path jar = path 159 .resolve(groupId.replace('.', '/') + '/' + artifactId + '/' + version + '/' + artifactId 160 + '-' + version + ".jar"); 161 if (Files.exists(jar)) { 162 return; 163 } 164 try { 165 if (!Files.isDirectory(jar.getParent())) { 166 Files.createDirectories(jar.getParent()); 167 } 168 } catch (final IOException e) { 169 throw new IllegalArgumentException( 170 "Can't create extension artifact " + groupId + ':' + artifactId + ':' + version, e); 171 } 172 try (final JarOutputStream stream = new JarOutputStream(new BufferedOutputStream( 173 Files.newOutputStream(jar, StandardOpenOption.CREATE, StandardOpenOption.WRITE)))) { 174 creator.accept(stream); 175 } catch (final IOException e) { 176 throw new IllegalStateException(e); 177 } 178 } 179 }, NotificationOptions.ofExecutor(r -> { 180 final Thread thread = new Thread(r, 181 ExtensionComponentMetadataManager.this.getClass().getName() + "-extension-registrar"); 182 thread.start(); 183 })).toCompletableFuture().get(configuration.getExtensionsStartupTimeout(), MILLISECONDS); 184 } catch (final InterruptedException e) { 185 Thread.currentThread().interrupt(); 186 } catch (final ExecutionException e) { 187 throw new IllegalStateException(e.getCause()); 188 } catch (final TimeoutException e) { 189 throw new IllegalStateException("Can't initialize extensions withing " 190 + MILLISECONDS.toSeconds(configuration.getExtensionsStartupTimeout()) + "s"); 191 } 192 } 193 194 private Stream<Link> createBuiltInLinks(final ComponentDetail componentDetail) { 195 return Stream 196 .of(new Link("Detail", "/component/details?identifiers=" + componentDetail.getId().getId(), 197 MediaType.APPLICATION_JSON)); 198 } 199 200 public String getFamilyIconFor(final String familyId) { 201 if (!isExtensionEntity(familyId)) { 202 throw new IllegalArgumentException(familyId + " is not a virtual family"); 203 } 204 return familyId.replace("::", "_"); 205 } 206 207 public boolean isExtensionEntity(final String id) { 208 return details.containsKey(id) || id.startsWith(EXTENSION_MARKER); 209 } 210 211 public Optional<Action> getAction(final String family, final String type, final String name) { 212 waitAndClearWaiters(); 213 return ofNullable(actions.get(new ActionKey(family, type, name))); 214 } 215 216 public Collection<Action> getActions() { 217 waitAndClearWaiters(); 218 return actions.values(); 219 } 220 221 public Collection<ConfigTypeNode> getConfigurations() { 222 waitAndClearWaiters(); 223 return configurations.values(); 224 } 225 226 public Collection<ComponentDetail> getDetails() { 227 waitAndClearWaiters(); 228 return details.values(); 229 } 230 231 public Optional<ComponentDetail> findComponentById(final String id) { 232 waitAndClearWaiters(); 233 return ofNullable(details.get(id)); 234 } 235 236 public DependencyDefinition getDependenciesFor(final String id) { 237 waitAndClearWaiters(); 238 return dependencies.get(id); 239 } 240 241 private void waitAndClearWaiters() { 242 if (waiters.isEmpty()) { 243 return; 244 } 245 synchronized (waiters) { 246 if (waiters.isEmpty()) { 247 return; 248 } 249 waiters.forEach(Runnable::run); 250 waiters.clear(); 251 } 252 } 253 254 /* 255 * private ConfigTypeNode findFamily(final ConfigTypeNode node, final Map<String, ConfigTypeNode> configs) { 256 * if (node.getParentId() == null) { 257 * return node; 258 * } 259 * String parentId = node.getParentId(); 260 * while (parentId != null) { 261 * final ConfigTypeNode parent = configs.get(parentId); 262 * if (parent == null) { 263 * return null; // error 264 * } 265 * parentId = parent.getParentId(); 266 * if (parentId == null) { 267 * return parent; 268 * } 269 * } 270 * return null; // error 271 * } 272 */ 273 274 private static class ActionKey { 275 276 private final String family; 277 278 private final String type; 279 280 private final String name; 281 282 private final int hash; 283 284 private ActionKey(final String family, final String type, final String name) { 285 this.family = family; 286 this.type = type; 287 this.name = name; 288 this.hash = Objects.hash(family, type, name); 289 } 290 291 @Override 292 public boolean equals(final Object o) { 293 if (this == o) { 294 return true; 295 } 296 if (o == null || getClass() != o.getClass()) { 297 return false; 298 } 299 final ActionKey actionKey = ActionKey.class.cast(o); 300 return hash == actionKey.hash && family.equals(actionKey.family) && type.equals(actionKey.type) 301 && name.equals(actionKey.name); 302 } 303 304 @Override 305 public int hashCode() { 306 return hash; 307 } 308 } 309}