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.front;
017
018import static java.util.Arrays.asList;
019import static java.util.Optional.ofNullable;
020import static java.util.function.Function.identity;
021import static java.util.stream.Collectors.toList;
022import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE;
023
024import java.util.Collection;
025import java.util.HashMap;
026import java.util.HashSet;
027import java.util.Locale;
028import java.util.Map;
029import java.util.concurrent.CompletableFuture;
030import java.util.concurrent.CompletionStage;
031import java.util.concurrent.ExecutionException;
032import java.util.function.Predicate;
033import java.util.stream.Stream;
034
035import javax.cache.annotation.CacheDefaults;
036import javax.cache.annotation.CacheResult;
037import javax.enterprise.context.ApplicationScoped;
038import javax.inject.Inject;
039import javax.ws.rs.WebApplicationException;
040import javax.ws.rs.core.Context;
041import javax.ws.rs.core.HttpHeaders;
042import javax.ws.rs.core.Response;
043
044import org.talend.sdk.component.api.exception.ComponentException;
045import org.talend.sdk.component.runtime.manager.ComponentManager;
046import org.talend.sdk.component.runtime.manager.ContainerComponentRegistry;
047import org.talend.sdk.component.runtime.manager.ServiceMeta;
048import org.talend.sdk.component.server.api.ActionResource;
049import org.talend.sdk.component.server.dao.ComponentActionDao;
050import org.talend.sdk.component.server.extension.api.action.Action;
051import org.talend.sdk.component.server.front.model.ActionItem;
052import org.talend.sdk.component.server.front.model.ActionList;
053import org.talend.sdk.component.server.front.model.ErrorDictionary;
054import org.talend.sdk.component.server.front.model.error.ErrorPayload;
055import org.talend.sdk.component.server.service.ExtensionComponentMetadataManager;
056import org.talend.sdk.component.server.service.LocaleMapper;
057import org.talend.sdk.component.server.service.PropertiesService;
058import org.talend.sdk.component.server.service.httpurlconnection.IgnoreNetAuthenticator;
059import org.talend.sdk.component.server.service.jcache.FrontCacheKeyGenerator;
060import org.talend.sdk.component.server.service.jcache.FrontCacheResolver;
061import org.talend.sdk.components.vault.client.VaultClient;
062
063import lombok.extern.slf4j.Slf4j;
064
065@Slf4j
066@ApplicationScoped
067@IgnoreNetAuthenticator
068@CacheDefaults(cacheResolverFactory = FrontCacheResolver.class, cacheKeyGenerator = FrontCacheKeyGenerator.class)
069public class ActionResourceImpl implements ActionResource {
070
071    @Inject
072    private ComponentManager manager;
073
074    @Inject
075    private ComponentActionDao actionDao;
076
077    @Inject
078    private PropertiesService propertiesService;
079
080    @Inject
081    private LocaleMapper localeMapper;
082
083    @Inject
084    private ExtensionComponentMetadataManager virtualActions;
085
086    @Inject
087    @Context
088    private HttpHeaders headers;
089
090    @Inject
091    private VaultClient vault;
092
093    @Override
094    public CompletionStage<Response> execute(final String family, final String type, final String action,
095            final String lang, final Map<String, String> params) {
096        return virtualActions
097                .getAction(family, type, action)
098                .map(it -> it.getHandler().apply(params, lang).exceptionally(this::onError))
099                .orElseGet(() -> doExecuteLocalAction(family, type, action, lang, params));
100    }
101
102    @Override
103    @CacheResult
104    public ActionList getIndex(final String[] types, final String[] families, final String language) {
105        final Predicate<String> typeMatcher = new Predicate<String>() {
106
107            private final Collection<String> accepted = new HashSet<>(asList(types));
108
109            @Override
110            public boolean test(final String type) {
111                return accepted.isEmpty() || accepted.contains(type);
112            }
113        };
114        final Predicate<String> componentMatcher = new Predicate<String>() {
115
116            private final Collection<String> accepted = new HashSet<>(asList(families));
117
118            @Override
119            public boolean test(final String family) {
120                return accepted.isEmpty() || accepted.contains(family);
121            }
122        };
123        final Locale locale = localeMapper.mapLocale(language);
124        return new ActionList(Stream
125                .concat(findDeployedActions(typeMatcher, componentMatcher, locale),
126                        findVirtualActions(typeMatcher, componentMatcher, locale))
127                .collect(toList()));
128    }
129
130    private CompletableFuture<Response> doExecuteLocalAction(final String family, final String type,
131            final String action, final String lang, final Map<String, String> params) {
132        return CompletableFuture.supplyAsync(() -> {
133            if (action == null) {
134                throw new WebApplicationException(Response
135                        .status(Response.Status.BAD_REQUEST)
136                        .entity(new ErrorPayload(ErrorDictionary.ACTION_MISSING, "Action can't be null"))
137                        .build());
138            }
139            final ServiceMeta.ActionMeta actionMeta = actionDao.findBy(family, type, action);
140            if (actionMeta == null) {
141                throw new WebApplicationException(Response
142                        .status(Response.Status.NOT_FOUND)
143                        .entity(new ErrorPayload(ErrorDictionary.ACTION_MISSING, "No action with id '" + action + "'"))
144                        .build());
145            }
146            try {
147                final Map<String, String> runtimeParams = ofNullable(params).map(HashMap::new).orElseGet(HashMap::new);
148                runtimeParams.put("$lang", localeMapper.mapLocale(lang).getLanguage());
149                String tenant;
150                try {
151                    tenant = headers.getHeaderString("x-talend-tenant-id");
152                } catch (Exception e) {
153                    log.debug("[doExecuteLocalAction] context not applicable: {}", e.getMessage());
154                    tenant = null;
155                }
156                final Map<String, String> deciphered = vault.decrypt(runtimeParams, tenant);
157                final Object result = actionMeta.getInvoker().apply(deciphered);
158                return Response.ok(result).type(APPLICATION_JSON_TYPE).build();
159            } catch (final RuntimeException re) {
160                return onError(re);
161            }
162            // synchronous, if needed we can move to async with timeout later but currently we don't want.
163            // check org.talend.sdk.component.server.service.ComponentManagerService.readCurrentLocale if you change it
164        }, Runnable::run).exceptionally(e -> {
165            final Throwable cause;
166            if (ExecutionException.class.isInstance(e.getCause())) {
167                cause = e.getCause().getCause();
168            } else {
169                cause = e.getCause();
170            }
171            if (WebApplicationException.class.isInstance(cause)) {
172                final WebApplicationException wae = WebApplicationException.class.cast(cause);
173                final Response response = wae.getResponse();
174                String message = "";
175                if (ErrorPayload.class.isInstance(wae.getResponse().getEntity())) {
176                    throw wae; // already logged and setup broken so just rethrow
177                } else {
178                    try {
179                        message = response.readEntity(String.class);
180                    } catch (final Exception ignored) {
181                        // no-op
182                    }
183                    if (message.isEmpty()) {
184                        message = cause.getMessage();
185                    }
186                    throw new WebApplicationException(message,
187                            Response
188                                    .status(response.getStatus())
189                                    .entity(new ErrorPayload(ErrorDictionary.UNEXPECTED, message))
190                                    .build());
191                }
192            }
193            throw new WebApplicationException(Response
194                    .status(500)
195                    .entity(new ErrorPayload(ErrorDictionary.UNEXPECTED, cause.getMessage()))
196                    .build());
197        });
198    }
199
200    private Response onError(final Throwable re) {
201        log.warn(re.getMessage(), re);
202        if (WebApplicationException.class.isInstance(re.getCause())) {
203            return WebApplicationException.class.cast(re.getCause()).getResponse();
204        }
205
206        if (ComponentException.class.isInstance(re)) {
207            final ComponentException ce = (ComponentException) re;
208            throw new WebApplicationException(Response
209                    .status(ce.getErrorOrigin() == ComponentException.ErrorOrigin.USER ? 400
210                            : ce.getErrorOrigin() == ComponentException.ErrorOrigin.BACKEND ? 456 : 520,
211                            "Unexpected callback error")
212                    .entity(new ErrorPayload(ErrorDictionary.ACTION_ERROR,
213                            "Action execution failed with: " + ofNullable(re.getMessage())
214                                    .orElseGet(() -> NullPointerException.class.isInstance(re) ? "unexpected null"
215                                            : "no error message")))
216                    .build());
217        }
218
219        throw new WebApplicationException(Response
220                .status(520, "Unexpected callback error")
221                .entity(new ErrorPayload(ErrorDictionary.ACTION_ERROR,
222                        "Action execution failed with: " + ofNullable(re.getMessage())
223                                .orElseGet(() -> NullPointerException.class.isInstance(re) ? "unexpected null"
224                                        : "no error message")))
225                .build());
226    }
227
228    private Stream<ActionItem> findVirtualActions(final Predicate<String> typeMatcher,
229            final Predicate<String> componentMatcher, final Locale locale) {
230        return virtualActions
231                .getActions()
232                .stream()
233                .filter(act -> typeMatcher.test(act.getReference().getType())
234                        && componentMatcher.test(act.getReference().getFamily()))
235                .map(Action::getReference)
236                .map(it -> new ActionItem(it.getFamily(), it.getType(), it.getName(), it.getProperties()));
237    }
238
239    private Stream<ActionItem> findDeployedActions(final Predicate<String> typeMatcher,
240            final Predicate<String> componentMatcher, final Locale locale) {
241        return manager
242                .find(c -> c
243                        .get(ContainerComponentRegistry.class)
244                        .getServices()
245                        .stream()
246                        .map(s -> s.getActions().stream())
247                        .flatMap(identity())
248                        .filter(act -> typeMatcher.test(act.getType()) && componentMatcher.test(act.getFamily()))
249                        .map(s -> new ActionItem(s.getFamily(), s.getType(), s.getAction(),
250                                propertiesService
251                                        .buildProperties(s.getParameters().get(), c.getLoader(), locale, null)
252                                        .collect(toList()))));
253    }
254}