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}