001/** 002 * Copyright (C) 2006-2023 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}