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}