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}