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.Collections.emptyMap; 019import static java.util.Optional.ofNullable; 020import static java.util.concurrent.CompletableFuture.completedFuture; 021import static java.util.stream.Collectors.joining; 022import static java.util.stream.Collectors.toList; 023import static java.util.stream.Collectors.toSet; 024 025import java.io.ByteArrayInputStream; 026import java.io.ByteArrayOutputStream; 027import java.io.IOException; 028import java.io.InputStream; 029import java.io.StringReader; 030import java.nio.charset.StandardCharsets; 031import java.security.Principal; 032import java.util.Collection; 033import java.util.Collections; 034import java.util.List; 035import java.util.Map; 036import java.util.concurrent.CompletableFuture; 037import java.util.concurrent.CompletionStage; 038import java.util.concurrent.ExecutionException; 039import java.util.stream.Stream; 040 041import javax.annotation.PostConstruct; 042import javax.enterprise.context.ApplicationScoped; 043import javax.inject.Inject; 044import javax.json.Json; 045import javax.json.JsonObject; 046import javax.json.bind.Jsonb; 047import javax.servlet.ServletContext; 048import javax.servlet.ServletException; 049import javax.servlet.http.HttpServletRequest; 050import javax.ws.rs.HttpMethod; 051import javax.ws.rs.WebApplicationException; 052import javax.ws.rs.core.Context; 053import javax.ws.rs.core.Response; 054import javax.ws.rs.core.UriInfo; 055 056import org.apache.cxf.Bus; 057import org.apache.cxf.transport.http.DestinationRegistry; 058import org.apache.cxf.transport.servlet.ServletController; 059import org.apache.cxf.transport.servlet.servicelist.ServiceListGeneratorServlet; 060import org.talend.sdk.component.server.api.BulkReadResource; 061import org.talend.sdk.component.server.front.cxf.CxfExtractor; 062import org.talend.sdk.component.server.front.memory.InMemoryRequest; 063import org.talend.sdk.component.server.front.memory.InMemoryResponse; 064import org.talend.sdk.component.server.front.memory.MemoryInputStream; 065import org.talend.sdk.component.server.front.memory.SimpleServletConfig; 066import org.talend.sdk.component.server.front.model.BulkRequests; 067import org.talend.sdk.component.server.front.model.BulkResponses; 068import org.talend.sdk.component.server.front.model.ErrorDictionary; 069import org.talend.sdk.component.server.front.model.error.ErrorPayload; 070import org.talend.sdk.component.server.service.qualifier.ComponentServer; 071 072import lombok.extern.slf4j.Slf4j; 073 074@Slf4j 075@ApplicationScoped 076public class BulkReadResourceImpl implements BulkReadResource { 077 078 private static final CompletableFuture[] EMPTY_PROMISES = new CompletableFuture[0]; 079 080 @Inject 081 private CxfExtractor cxf; 082 083 @Inject 084 private Bus bus; 085 086 @Inject 087 @Context 088 private ServletContext servletContext; 089 090 @Inject 091 @Context 092 private HttpServletRequest httpServletRequest; 093 094 @Inject 095 @Context 096 private UriInfo uriInfo; 097 098 @Inject 099 @Context 100 private HttpServletRequest request; 101 102 @Inject 103 @ComponentServer 104 private Jsonb defaultMapper; 105 106 private ServletController controller; 107 108 private final String appPrefix = "/api/v1"; 109 110 private final Collection<String> blacklisted = 111 Stream.of(appPrefix + "/component/icon/", appPrefix + "/component/dependency/").collect(toSet()); 112 113 private final BulkResponses.Result forbiddenInBulkModeResponse = 114 new BulkResponses.Result(Response.Status.FORBIDDEN.getStatusCode(), emptyMap(), 115 Json.createReader(new StringReader( 116 "{\"code\":\"UNAUTHORIZED\",\"description\":\"Forbidden endpoint in bulk mode.\"}")) 117 .readObject()); 118 119 private final BulkResponses.Result forbiddenResponse = 120 new BulkResponses.Result(Response.Status.FORBIDDEN.getStatusCode(), emptyMap(), 121 Json.createReader(new StringReader( 122 "{\"code\":\"UNAUTHORIZED\",\"description\":\"Secured endpoint, ensure to pass the right token.\"}")) 123 .readObject()); 124 125 private final BulkResponses.Result invalidResponse = 126 new BulkResponses.Result(Response.Status.BAD_REQUEST.getStatusCode(), emptyMap(), 127 Json.createReader( 128 new StringReader("{\"code\":\"UNEXPECTED\",\"description\":\"unknownEndpoint.\"}")) 129 .readObject()); 130 131 @PostConstruct 132 private void init() { 133 final DestinationRegistry registry = cxf.getRegistry(); 134 controller = new ServletController(registry, 135 new SimpleServletConfig(servletContext, "Talend Component Kit Bulk Transport"), 136 new ServiceListGeneratorServlet(registry, bus)); 137 } 138 139 @Override 140 public CompletionStage<BulkResponses> bulk(final BulkRequests requests) { 141 final Collection<CompletableFuture<BulkResponses.Result>> responses = 142 ofNullable(requests.getRequests()).map(Collection::stream).orElseGet(Stream::empty).map(request -> { 143 if (isBlacklisted(request)) { 144 return completedFuture(forbiddenInBulkModeResponse); 145 } 146 if (request.getPath() == null || !request.getPath().startsWith(appPrefix) 147 || request.getPath().contains("?")) { 148 return completedFuture(invalidResponse); 149 } 150 return doExecute(request, uriInfo); 151 }).collect(toList()); 152 return CompletableFuture 153 .allOf(responses.toArray(EMPTY_PROMISES)) 154 .handle((ignored, error) -> new BulkResponses(responses.stream().map(it -> { 155 try { 156 return it.get(); 157 } catch (final InterruptedException e) { 158 Thread.currentThread().interrupt(); 159 throw new IllegalStateException(e); 160 } catch (final ExecutionException e) { 161 throw new WebApplicationException(Response 162 .serverError() 163 .entity(new ErrorPayload(ErrorDictionary.UNEXPECTED, e.getMessage())) 164 .build()); 165 } 166 }).collect(toList()))); 167 } 168 169 private boolean isBlacklisted(final BulkRequests.Request request) { 170 return blacklisted.stream().anyMatch(it -> request.getPath() == null || request.getPath().startsWith(it)); 171 } 172 173 private CompletableFuture<BulkResponses.Result> doExecute(final BulkRequests.Request inputRequest, 174 final UriInfo info) { 175 final Map<String, List<String>> headers = 176 ofNullable(inputRequest.getHeaders()).orElseGet(Collections::emptyMap); 177 final String path = ofNullable(inputRequest.getPath()).map(it -> it.substring(appPrefix.length())).orElse("/"); 178 179 // theoretically we should encode these params but should be ok this way for now - due to the param we can 180 // accept 181 final String queryString = ofNullable(inputRequest.getQueryParameters()) 182 .map(Map::entrySet) 183 .map(Collection::stream) 184 .orElseGet(Stream::empty) 185 .flatMap(it -> ofNullable(it.getValue()) 186 .map(Collection::stream) 187 .orElseGet(Stream::empty) 188 .map(value -> it.getKey() + '=' + value)) 189 .collect(joining("&")); 190 191 final int port = info.getBaseUri().getPort(); 192 final Principal userPrincipal = request.getUserPrincipal(); // this is ap proxy so ready it early 193 final InMemoryRequest request = new InMemoryRequest(ofNullable(inputRequest.getVerb()).orElse(HttpMethod.GET), 194 headers, path, appPrefix + path, appPrefix, queryString, port < 0 ? 8080 : port, servletContext, 195 new MemoryInputStream(ofNullable(inputRequest.getPayload()) 196 .map(it -> it.getBytes(StandardCharsets.UTF_8)) 197 .map(ByteArrayInputStream::new) 198 .map(InputStream.class::cast) 199 .orElse(null)), 200 () -> userPrincipal, controller); 201 final BulkResponses.Result result = new BulkResponses.Result(); 202 final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 203 final CompletableFuture<BulkResponses.Result> promise = new CompletableFuture<>(); 204 final InMemoryResponse response = new InMemoryResponse(() -> true, () -> { 205 try { 206 JsonObject jsonObject = Json.createReader(new StringReader(outputStream.toString())).readObject(); 207 result.setResponse(jsonObject); 208 } catch (Exception e) { 209 throw new RuntimeException(e); 210 } 211 promise.complete(result); 212 }, bytes -> { 213 try { 214 outputStream.write(bytes); 215 } catch (final IOException e) { 216 throw new IllegalStateException(e); 217 } 218 }, (status, responseHeaders) -> { 219 result.setStatus(status); 220 result.setHeaders(headers); 221 return ""; 222 }); 223 request.setResponse(response); 224 try { 225 controller.invoke(request, response); 226 } catch (final ServletException e) { 227 result.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); 228 result.setResponse(Json.createReader(new StringReader(defaultMapper 229 .toJson(new ErrorPayload(ErrorDictionary.UNEXPECTED, e.getMessage())))) 230 .readObject()); 231 promise.complete(result); 232 throw new IllegalStateException(e); 233 } 234 return promise; 235 } 236}