001/** 002 * Copyright (C) 2006-2025 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 if (type == null) { 140 throw new WebApplicationException(Response 141 .status(Response.Status.BAD_REQUEST) 142 .entity(new ErrorPayload(ErrorDictionary.TYPE_MISSING, "Type can't be null")) 143 .build()); 144 } 145 if (family == null) { 146 throw new WebApplicationException(Response 147 .status(Response.Status.BAD_REQUEST) 148 .entity(new ErrorPayload(ErrorDictionary.FAMILY_MISSING, "Family can't be null")) 149 .build()); 150 } 151 final ServiceMeta.ActionMeta actionMeta = actionDao.findBy(family, type, action); 152 if (actionMeta == null) { 153 throw new WebApplicationException(Response 154 .status(Response.Status.NOT_FOUND) 155 .entity(new ErrorPayload(ErrorDictionary.ACTION_MISSING, "No action with id '" + action + "'")) 156 .build()); 157 } 158 try { 159 final Map<String, String> runtimeParams = ofNullable(params).map(HashMap::new).orElseGet(HashMap::new); 160 runtimeParams.put("$lang", localeMapper.mapLocale(lang).getLanguage()); 161 String tenant; 162 try { 163 tenant = headers.getHeaderString("x-talend-tenant-id"); 164 } catch (Exception e) { 165 log.debug("[doExecuteLocalAction] context not applicable: {}", e.getMessage()); 166 tenant = null; 167 } 168 final Map<String, String> deciphered = secUtils.decrypt(actionMeta.getParameters() 169 .get(), runtimeParams, tenant); 170 final Object result = actionMeta.getInvoker().apply(deciphered); 171 return Response.ok(result).type(APPLICATION_JSON_TYPE).build(); 172 } catch (final RuntimeException re) { 173 return onError(re); 174 } 175 // synchronous, if needed we can move to async with timeout later but currently we don't want. 176 // check org.talend.sdk.component.server.service.ComponentManagerService.readCurrentLocale if you change it 177 }, Runnable::run).exceptionally(e -> { 178 final Throwable cause; 179 if (ExecutionException.class.isInstance(e.getCause())) { 180 cause = e.getCause().getCause(); 181 } else { 182 cause = e.getCause(); 183 } 184 if (WebApplicationException.class.isInstance(cause)) { 185 final WebApplicationException wae = WebApplicationException.class.cast(cause); 186 final Response response = wae.getResponse(); 187 String message = ""; 188 if (ErrorPayload.class.isInstance(wae.getResponse().getEntity())) { 189 throw wae; // already logged and setup broken so just rethrow 190 } else { 191 try { 192 message = response.readEntity(String.class); 193 } catch (final Exception ignored) { 194 // no-op 195 } 196 if (message.isEmpty()) { 197 message = cause.getMessage(); 198 } 199 throw new WebApplicationException(message, 200 Response 201 .status(response.getStatus()) 202 .entity(new ErrorPayload(ErrorDictionary.UNEXPECTED, message)) 203 .build()); 204 } 205 } 206 throw new WebApplicationException(Response 207 .status(500) 208 .entity(new ErrorPayload(ErrorDictionary.UNEXPECTED, cause.getMessage())) 209 .build()); 210 }); 211 } 212 213 private Response onError(final Throwable re) { 214 log.warn(re.getMessage(), re); 215 if (WebApplicationException.class.isInstance(re.getCause())) { 216 return WebApplicationException.class.cast(re.getCause()).getResponse(); 217 } 218 219 if (ComponentException.class.isInstance(re)) { 220 final ComponentException ce = (ComponentException) re; 221 throw new WebApplicationException(Response 222 .status(ce.getErrorOrigin() == ComponentException.ErrorOrigin.USER ? 400 223 : ce.getErrorOrigin() == ComponentException.ErrorOrigin.BACKEND ? 456 : 520, 224 "Unexpected callback error") 225 .entity(new ErrorPayload(ErrorDictionary.ACTION_ERROR, 226 "Action execution failed with: " + ofNullable(re.getMessage()) 227 .orElseGet(() -> NullPointerException.class.isInstance(re) ? "unexpected null" 228 : "no error message"))) 229 .build()); 230 } 231 232 throw new WebApplicationException(Response 233 .status(520, "Unexpected callback error") 234 .entity(new ErrorPayload(ErrorDictionary.ACTION_ERROR, 235 "Action execution failed with: " + ofNullable(re.getMessage()) 236 .orElseGet(() -> NullPointerException.class.isInstance(re) ? "unexpected null" 237 : "no error message"))) 238 .build()); 239 } 240 241 private Stream<ActionItem> findVirtualActions(final Predicate<String> typeMatcher, 242 final Predicate<String> componentMatcher, final Locale locale) { 243 return virtualActions 244 .getActions() 245 .stream() 246 .filter(act -> typeMatcher.test(act.getReference().getType()) 247 && componentMatcher.test(act.getReference().getFamily())) 248 .map(Action::getReference) 249 .map(it -> new ActionItem(it.getFamily(), it.getType(), it.getName(), it.getProperties())); 250 } 251 252 private Stream<ActionItem> findDeployedActions(final Predicate<String> typeMatcher, 253 final Predicate<String> componentMatcher, final Locale locale) { 254 return manager 255 .find(c -> c 256 .get(ContainerComponentRegistry.class) 257 .getServices() 258 .stream() 259 .map(s -> s.getActions().stream()) 260 .flatMap(identity()) 261 .filter(act -> typeMatcher.test(act.getType()) && componentMatcher.test(act.getFamily())) 262 .map(s -> new ActionItem(s.getFamily(), s.getType(), s.getAction(), 263 propertiesService 264 .buildProperties(s.getParameters().get(), c.getLoader(), locale, null) 265 .collect(toList())))); 266 } 267}