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.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.function.Predicate;
032import java.util.stream.Stream;
033
034import javax.enterprise.context.ApplicationScoped;
035import javax.inject.Inject;
036import javax.ws.rs.WebApplicationException;
037import javax.ws.rs.core.Response;
038
039import org.talend.sdk.component.api.exception.ComponentException;
040import org.talend.sdk.component.runtime.manager.ComponentManager;
041import org.talend.sdk.component.runtime.manager.ContainerComponentRegistry;
042import org.talend.sdk.component.runtime.manager.ServiceMeta;
043import org.talend.sdk.component.server.api.ActionResource;
044import org.talend.sdk.component.server.dao.ComponentActionDao;
045import org.talend.sdk.component.server.extension.api.action.Action;
046import org.talend.sdk.component.server.front.model.ActionItem;
047import org.talend.sdk.component.server.front.model.ActionList;
048import org.talend.sdk.component.server.front.model.ErrorDictionary;
049import org.talend.sdk.component.server.front.model.error.ErrorPayload;
050import org.talend.sdk.component.server.service.ExtensionComponentMetadataManager;
051import org.talend.sdk.component.server.service.LocaleMapper;
052import org.talend.sdk.component.server.service.PropertiesService;
053import org.talend.sdk.component.server.service.httpurlconnection.IgnoreNetAuthenticator;
054
055import lombok.extern.slf4j.Slf4j;
056
057@Slf4j
058@ApplicationScoped
059@IgnoreNetAuthenticator
060public class ActionResourceImpl implements ActionResource {
061
062    @Inject
063    private ComponentManager manager;
064
065    @Inject
066    private ComponentActionDao actionDao;
067
068    @Inject
069    private PropertiesService propertiesService;
070
071    @Inject
072    private LocaleMapper localeMapper;
073
074    @Inject
075    private ExtensionComponentMetadataManager virtualActions;
076
077    @Override
078    public CompletionStage<Response> execute(final String family, final String type, final String action,
079            final String lang, final Map<String, String> params) {
080        return virtualActions
081                .getAction(family, type, action)
082                .map(it -> it.getHandler().apply(params, lang).exceptionally(this::onError))
083                .orElseGet(() -> doExecuteLocalAction(family, type, action, lang, params));
084    }
085
086    @Override
087    public ActionList getIndex(final String[] types, final String[] families, final String language) {
088        final Predicate<String> typeMatcher = new Predicate<String>() {
089
090            private final Collection<String> accepted = new HashSet<>(asList(types));
091
092            @Override
093            public boolean test(final String type) {
094                return accepted.isEmpty() || accepted.contains(type);
095            }
096        };
097        final Predicate<String> componentMatcher = new Predicate<String>() {
098
099            private final Collection<String> accepted = new HashSet<>(asList(families));
100
101            @Override
102            public boolean test(final String family) {
103                return accepted.isEmpty() || accepted.contains(family);
104            }
105        };
106        final Locale locale = localeMapper.mapLocale(language);
107        return new ActionList(Stream
108                .concat(findDeployedActions(typeMatcher, componentMatcher, locale),
109                        findVirtualActions(typeMatcher, componentMatcher, locale))
110                .collect(toList()));
111    }
112
113    private CompletableFuture<Response> doExecuteLocalAction(final String family, final String type,
114            final String action, final String lang, final Map<String, String> params) {
115        return CompletableFuture.supplyAsync(() -> {
116            if (action == null) {
117                throw new WebApplicationException(Response
118                        .status(Response.Status.BAD_REQUEST)
119                        .entity(new ErrorPayload(ErrorDictionary.ACTION_MISSING, "Action can't be null"))
120                        .build());
121            }
122            final ServiceMeta.ActionMeta actionMeta = actionDao.findBy(family, type, action);
123            if (actionMeta == null) {
124                throw new WebApplicationException(Response
125                        .status(Response.Status.NOT_FOUND)
126                        .entity(new ErrorPayload(ErrorDictionary.ACTION_MISSING, "No action with id '" + action + "'"))
127                        .build());
128            }
129            try {
130                final Map<String, String> runtimeParams = ofNullable(params).map(HashMap::new).orElseGet(HashMap::new);
131                runtimeParams.put("$lang", localeMapper.mapLocale(lang).getLanguage());
132                final Object result = actionMeta.getInvoker().apply(runtimeParams);
133                return Response.ok(result).type(APPLICATION_JSON_TYPE).build();
134            } catch (final RuntimeException re) {
135                return onError(re);
136            }
137            // synchronous, if needed we can move to async with timeout later but currently we don't want.
138            // check org.talend.sdk.component.server.service.ComponentManagerService.readCurrentLocale if you change it
139        }, Runnable::run);
140    }
141
142    private Response onError(final Throwable re) {
143        log.warn(re.getMessage(), re);
144        if (WebApplicationException.class.isInstance(re.getCause())) {
145            return WebApplicationException.class.cast(re.getCause()).getResponse();
146        }
147
148        if (ComponentException.class.isInstance(re)) {
149            ComponentException ce = (ComponentException) re;
150            throw new WebApplicationException(Response
151                    .status(ce.getErrorOrigin() == ComponentException.ErrorOrigin.USER ? 400
152                            : ce.getErrorOrigin() == ComponentException.ErrorOrigin.BACKEND ? 456 : 520,
153                            "Unexpected callback error")
154                    .entity(new ErrorPayload(ErrorDictionary.ACTION_ERROR,
155                            "Action execution failed with: " + ofNullable(re.getMessage())
156                                    .orElseGet(() -> NullPointerException.class.isInstance(re) ? "unexpected null"
157                                            : "no error message")))
158                    .build());
159        }
160
161        throw new WebApplicationException(Response
162                .status(520, "Unexpected callback error")
163                .entity(new ErrorPayload(ErrorDictionary.ACTION_ERROR,
164                        "Action execution failed with: " + ofNullable(re.getMessage())
165                                .orElseGet(() -> NullPointerException.class.isInstance(re) ? "unexpected null"
166                                        : "no error message")))
167                .build());
168    }
169
170    private Stream<ActionItem> findVirtualActions(final Predicate<String> typeMatcher,
171            final Predicate<String> componentMatcher, final Locale locale) {
172        return virtualActions
173                .getActions()
174                .stream()
175                .filter(act -> typeMatcher.test(act.getReference().getType())
176                        && componentMatcher.test(act.getReference().getFamily()))
177                .map(Action::getReference)
178                .map(it -> new ActionItem(it.getFamily(), it.getType(), it.getName(), it.getProperties()));
179    }
180
181    private Stream<ActionItem> findDeployedActions(final Predicate<String> typeMatcher,
182            final Predicate<String> componentMatcher, final Locale locale) {
183        return manager
184                .find(c -> c
185                        .get(ContainerComponentRegistry.class)
186                        .getServices()
187                        .stream()
188                        .map(s -> s.getActions().stream())
189                        .flatMap(identity())
190                        .filter(act -> typeMatcher.test(act.getType()) && componentMatcher.test(act.getFamily()))
191                        .map(s -> new ActionItem(s.getFamily(), s.getType(), s.getAction(),
192                                propertiesService
193                                        .buildProperties(s.getParameters().get(), c.getLoader(), locale, null)
194                                        .collect(toList()))));
195    }
196}