001/**
002 * Copyright (C) 2006-2022 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.front.security.SecurityUtils;
056import org.talend.sdk.component.server.service.ExtensionComponentMetadataManager;
057import org.talend.sdk.component.server.service.LocaleMapper;
058import org.talend.sdk.component.server.service.PropertiesService;
059import org.talend.sdk.component.server.service.httpurlconnection.IgnoreNetAuthenticator;
060import org.talend.sdk.component.server.service.jcache.FrontCacheKeyGenerator;
061import org.talend.sdk.component.server.service.jcache.FrontCacheResolver;
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 SecurityUtils secUtils;
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 = secUtils.decrypt(actionMeta.getParameters()
157                        .get(), runtimeParams, tenant);
158                final Object result = actionMeta.getInvoker().apply(deciphered);
159                return Response.ok(result).type(APPLICATION_JSON_TYPE).build();
160            } catch (final RuntimeException re) {
161                return onError(re);
162            }
163            // synchronous, if needed we can move to async with timeout later but currently we don't want.
164            // check org.talend.sdk.component.server.service.ComponentManagerService.readCurrentLocale if you change it
165        }, Runnable::run).exceptionally(e -> {
166            final Throwable cause;
167            if (ExecutionException.class.isInstance(e.getCause())) {
168                cause = e.getCause().getCause();
169            } else {
170                cause = e.getCause();
171            }
172            if (WebApplicationException.class.isInstance(cause)) {
173                final WebApplicationException wae = WebApplicationException.class.cast(cause);
174                final Response response = wae.getResponse();
175                String message = "";
176                if (ErrorPayload.class.isInstance(wae.getResponse().getEntity())) {
177                    throw wae; // already logged and setup broken so just rethrow
178                } else {
179                    try {
180                        message = response.readEntity(String.class);
181                    } catch (final Exception ignored) {
182                        // no-op
183                    }
184                    if (message.isEmpty()) {
185                        message = cause.getMessage();
186                    }
187                    throw new WebApplicationException(message,
188                            Response
189                                    .status(response.getStatus())
190                                    .entity(new ErrorPayload(ErrorDictionary.UNEXPECTED, message))
191                                    .build());
192                }
193            }
194            throw new WebApplicationException(Response
195                    .status(500)
196                    .entity(new ErrorPayload(ErrorDictionary.UNEXPECTED, cause.getMessage()))
197                    .build());
198        });
199    }
200
201    private Response onError(final Throwable re) {
202        log.warn(re.getMessage(), re);
203        if (WebApplicationException.class.isInstance(re.getCause())) {
204            return WebApplicationException.class.cast(re.getCause()).getResponse();
205        }
206
207        if (ComponentException.class.isInstance(re)) {
208            final ComponentException ce = (ComponentException) re;
209            throw new WebApplicationException(Response
210                    .status(ce.getErrorOrigin() == ComponentException.ErrorOrigin.USER ? 400
211                            : ce.getErrorOrigin() == ComponentException.ErrorOrigin.BACKEND ? 456 : 520,
212                            "Unexpected callback error")
213                    .entity(new ErrorPayload(ErrorDictionary.ACTION_ERROR,
214                            "Action execution failed with: " + ofNullable(re.getMessage())
215                                    .orElseGet(() -> NullPointerException.class.isInstance(re) ? "unexpected null"
216                                            : "no error message")))
217                    .build());
218        }
219
220        throw new WebApplicationException(Response
221                .status(520, "Unexpected callback error")
222                .entity(new ErrorPayload(ErrorDictionary.ACTION_ERROR,
223                        "Action execution failed with: " + ofNullable(re.getMessage())
224                                .orElseGet(() -> NullPointerException.class.isInstance(re) ? "unexpected null"
225                                        : "no error message")))
226                .build());
227    }
228
229    private Stream<ActionItem> findVirtualActions(final Predicate<String> typeMatcher,
230            final Predicate<String> componentMatcher, final Locale locale) {
231        return virtualActions
232                .getActions()
233                .stream()
234                .filter(act -> typeMatcher.test(act.getReference().getType())
235                        && componentMatcher.test(act.getReference().getFamily()))
236                .map(Action::getReference)
237                .map(it -> new ActionItem(it.getFamily(), it.getType(), it.getName(), it.getProperties()));
238    }
239
240    private Stream<ActionItem> findDeployedActions(final Predicate<String> typeMatcher,
241            final Predicate<String> componentMatcher, final Locale locale) {
242        return manager
243                .find(c -> c
244                        .get(ContainerComponentRegistry.class)
245                        .getServices()
246                        .stream()
247                        .map(s -> s.getActions().stream())
248                        .flatMap(identity())
249                        .filter(act -> typeMatcher.test(act.getType()) && componentMatcher.test(act.getFamily()))
250                        .map(s -> new ActionItem(s.getFamily(), s.getType(), s.getAction(),
251                                propertiesService
252                                        .buildProperties(s.getParameters().get(), c.getLoader(), locale, null)
253                                        .collect(toList()))));
254    }
255}