All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.datarouter.httpclient.client.StandardDatarouterHttpClient Maven / Gradle / Ivy

There is a newer version: 0.0.126
Show newest version
/*
 * Copyright © 2009 HotPads ([email protected])
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.datarouter.httpclient.client;

import java.io.IOException;
import java.lang.reflect.Type;
import java.net.URI;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import javax.inject.Singleton;

import org.apache.http.HttpEntity;
import org.apache.http.HttpStatus;
import org.apache.http.client.CookieStore;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.entity.ContentType;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.impl.cookie.BasicClientCookie;
import org.apache.http.pool.PoolStats;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.datarouter.httpclient.circuitbreaker.DatarouterHttpClientIoExceptionCircuitBreaker;
import io.datarouter.httpclient.endpoint.BaseEndpoint;
import io.datarouter.httpclient.endpoint.EndpointTool;
import io.datarouter.httpclient.json.JsonSerializer;
import io.datarouter.httpclient.request.DatarouterHttpRequest;
import io.datarouter.httpclient.request.HttpRequestMethod;
import io.datarouter.httpclient.response.Conditional;
import io.datarouter.httpclient.response.DatarouterHttpResponse;
import io.datarouter.httpclient.response.exception.DatarouterHttpException;
import io.datarouter.httpclient.response.exception.DatarouterHttpResponseException;
import io.datarouter.httpclient.response.exception.DatarouterHttpRuntimeException;
import io.datarouter.httpclient.security.CsrfGenerator;
import io.datarouter.httpclient.security.CsrfGenerator.RefreshableCsrfGenerator;
import io.datarouter.httpclient.security.SecurityParameters;
import io.datarouter.httpclient.security.SignatureGenerator;
import io.datarouter.httpclient.security.SignatureGenerator.RefreshableSignatureGenerator;
import io.datarouter.instrumentation.refreshable.RefreshableSupplier;
import io.datarouter.instrumentation.trace.TraceSpanFinisher;
import io.datarouter.instrumentation.trace.TraceSpanGroupType;
import io.datarouter.instrumentation.trace.TracerTool;

@Singleton
public class StandardDatarouterHttpClient implements DatarouterHttpClient{
	private static final Logger logger = LoggerFactory.getLogger(StandardDatarouterHttpClient.class);

	private final CloseableHttpClient httpClient;
	private final JsonSerializer jsonSerializer;
	private final SignatureGenerator signatureGenerator;
	private final CsrfGenerator csrfGenerator;
	private final Supplier apiKeySupplier;
	private final RefreshableSignatureGenerator refreshableSignatureGenerator;
	private final RefreshableCsrfGenerator refreshableCsrfGenerator;
	private final RefreshableSupplier refreshableApiKeySupplier;
	private final DatarouterHttpClientConfig config;
	private final PoolingHttpClientConnectionManager connectionManager;
	private final DatarouterHttpClientIoExceptionCircuitBreaker circuitWrappedHttpClient;
	private final Supplier enableBreakers;
	private final Supplier urlPrefix;

	StandardDatarouterHttpClient(
			CloseableHttpClient httpClient,
			JsonSerializer jsonSerializer,
			SignatureGenerator signatureGenerator,
			CsrfGenerator csrfGenerator,
			Supplier apiKeySupplier,
			RefreshableSignatureGenerator refreshableSignatureGenerator,
			RefreshableCsrfGenerator refreshableCsrfGenerator,
			RefreshableSupplier refreshableApiKeySupplier,
			DatarouterHttpClientConfig config,
			PoolingHttpClientConnectionManager connectionManager,
			String name,
			Supplier enableBreakers,
			Supplier urlPrefix){
		this.httpClient = httpClient;
		this.jsonSerializer = jsonSerializer;
		this.signatureGenerator = signatureGenerator;
		this.csrfGenerator = csrfGenerator;
		this.apiKeySupplier = apiKeySupplier;
		this.refreshableSignatureGenerator = refreshableSignatureGenerator;
		this.refreshableCsrfGenerator = refreshableCsrfGenerator;
		this.refreshableApiKeySupplier = refreshableApiKeySupplier;
		this.config = config;
		this.connectionManager = connectionManager;
		this.circuitWrappedHttpClient = new DatarouterHttpClientIoExceptionCircuitBreaker(name);
		this.enableBreakers = enableBreakers;
		this.urlPrefix = urlPrefix;
	}

	@Override
	public DatarouterHttpResponse execute(DatarouterHttpRequest request){
		try{
			return executeChecked(request);
		}catch(DatarouterHttpException e){
			throw new DatarouterHttpRuntimeException(e);
		}
	}

	@Override
	public DatarouterHttpResponse execute(DatarouterHttpRequest request, Consumer httpEntityConsumer){
		try{
			return executeChecked(request, httpEntityConsumer);
		}catch(DatarouterHttpException e){
			throw new DatarouterHttpRuntimeException(e);
		}
	}

	@Override
	public  E execute(DatarouterHttpRequest request, Type deserializeToType){
		try{
			return executeChecked(request, deserializeToType);
		}catch(DatarouterHttpException e){
			throw new DatarouterHttpRuntimeException(e);
		}
	}

	@Override
	public  E executeChecked(DatarouterHttpRequest request, Type deserializeToType) throws DatarouterHttpException{
		String entity = executeChecked(request).getEntity();
		try(TraceSpanFinisher $ = TracerTool.startSpan("JsonSerializer deserialize", TraceSpanGroupType.SERIALIZATION)){
			TracerTool.appendToSpanInfo("characters", entity.length());
			return jsonSerializer.deserialize(entity, deserializeToType);
		}
	}

	@Override
	public DatarouterHttpResponse executeChecked(DatarouterHttpRequest request) throws DatarouterHttpException{
		return executeChecked(request, (Consumer)null);
	}

	@Override
	public DatarouterHttpResponse executeChecked(DatarouterHttpRequest request, Consumer httpEntityConsumer)
	throws DatarouterHttpException{
		//store info needed to retry
		Instant firstAttemptInstant = Instant.now();
		Map> originalGetParams = request.getGetParams().entrySet().stream()
				.collect(Collectors.toMap(Entry::getKey, entry -> new ArrayList<>(entry.getValue())));
		Map> originalPostParams = request.getPostParams().entrySet().stream()
				.collect(Collectors.toMap(Entry::getKey, entry -> new ArrayList<>(entry.getValue())));
		Map> originalHeaders = request.getHeaders().entrySet().stream()
				.collect(Collectors.toMap(Entry::getKey, entry -> new ArrayList<>(entry.getValue())));
		try{
			return executeCheckedInternal(request, httpEntityConsumer);
		}catch(DatarouterHttpResponseException e){
			if(shouldRerun40x(firstAttemptInstant, e.getResponse().getStatusCode(), request.getShouldSkipSecurity())){
				//reset any changes to request made during the first attempt
				request.setGetParams(originalGetParams);
				request.setPostParams(originalPostParams);
				request.setHeaders(originalHeaders);
				logger.warn("retrying {}", e.getResponse().getStatusCode());
				return executeCheckedInternal(request, httpEntityConsumer);
			}
			throw e;
		}
	}

	private DatarouterHttpResponse executeCheckedInternal(DatarouterHttpRequest request,
			Consumer httpEntityConsumer) throws DatarouterHttpException{
		setSecurityProperties(request);

		HttpClientContext context = new HttpClientContext();
		context.setAttribute(HttpRetryTool.RETRY_SAFE_ATTRIBUTE, request.getRetrySafe());
		CookieStore cookieStore = new BasicCookieStore();
		for(BasicClientCookie cookie : request.getCookies()){
			cookieStore.addCookie(cookie);
		}
		context.setCookieStore(cookieStore);
		return circuitWrappedHttpClient.call(httpClient, request, httpEntityConsumer, context, enableBreakers);
	}

	@Override
	public Conditional tryExecute(DatarouterHttpRequest request){
		DatarouterHttpResponse response;
		try{
			response = executeChecked(request);
		}catch(DatarouterHttpException e){
			return Conditional.failure(e);
		}
		return Conditional.success(response);
	}

	@Override
	public Conditional tryExecute(
			DatarouterHttpRequest request,
			Consumer httpEntityConsumer){
		DatarouterHttpResponse response;
		try{
			response = executeChecked(request, httpEntityConsumer);
		}catch(DatarouterHttpException e){
			return Conditional.failure(e);
		}
		return Conditional.success(response);
	}

	@Override
	public  Conditional tryExecute(DatarouterHttpRequest request, Type deserializeToType){
		E response;
		try{
			response = executeChecked(request, deserializeToType);
		}catch(DatarouterHttpException e){
			if(!request.getShouldSkipLogs()){
				logger.warn("", e);
			}
			return Conditional.failure(e);
		}
		return Conditional.success(response);
	}

	@Override
	public  Conditional call(BaseEndpoint endpoint){
		initUrlPrefix(endpoint);
		DatarouterHttpRequest datarouterHttpRequest = EndpointTool.toDatarouterHttpRequest(endpoint);
		EndpointTool.findEntity(endpoint).ifPresent(entity -> setEntityDto(datarouterHttpRequest, entity));
		return tryExecute(datarouterHttpRequest, endpoint.responseType);
	}

	private void setSecurityProperties(DatarouterHttpRequest request){
		if(request.getShouldSkipSecurity()){
			//the only case from below that is relevant without security is populating the entity
			if(request.canHaveEntity() && request.getEntity() == null){
				request.setEntity(request.getFirstPostParams());
			}
			return;
		}
		SignatureGenerator signatureGenerator = chooseSignatureGenerator();
		CsrfGenerator csrfGenerator = chooseCsrfGenerator();
		Supplier apiKeySupplier = chooseApiKeySupplier();

		Map params = new HashMap<>();
		if(csrfGenerator != null){
			String csrfIv = csrfGenerator.generateCsrfIv();
			params.put(SecurityParameters.CSRF_IV, csrfIv);
			params.put(SecurityParameters.CSRF_TOKEN, csrfGenerator.generateCsrfToken(csrfIv));
		}

		if(apiKeySupplier != null){
			params.put(SecurityParameters.API_KEY, apiKeySupplier.get());
		}
		if(request.canHaveEntity() && request.getEntity() == null){
			params = request.addPostParams(params).getFirstPostParams();
			if(signatureGenerator != null && !params.isEmpty()){
				String signature = signatureGenerator.getHexSignature(request.getFirstPostParams()).signature;
				request.addPostParam(SecurityParameters.SIGNATURE, signature);
			}
			request.setEntity(request.getFirstPostParams());
		}else if(request.getMethod() == HttpRequestMethod.GET){
			params = request.addGetParams(params).getFirstGetParams();
			if(signatureGenerator != null && !params.isEmpty()){
				String signature = signatureGenerator.getHexSignature(request.getFirstGetParams()).signature;
				request.addGetParam(SecurityParameters.SIGNATURE, signature);
			}
		}else{
			request.addHeaders(params);
			if(signatureGenerator != null && request.getEntity() != null){
				String signature = signatureGenerator.getHexSignature(
						request.getFirstGetParams(),
						request.getEntity()).signature;
				request.addHeader(SecurityParameters.SIGNATURE, signature);
			}
		}
	}

	private Supplier chooseApiKeySupplier(){
		return refreshableApiKeySupplier != null ? refreshableApiKeySupplier : apiKeySupplier != null ? apiKeySupplier
				: null;
	}

	private SignatureGenerator chooseSignatureGenerator(){
		return refreshableSignatureGenerator != null ? refreshableSignatureGenerator : signatureGenerator != null
				? signatureGenerator : null;
	}

	private CsrfGenerator chooseCsrfGenerator(){
		return refreshableCsrfGenerator != null ? refreshableCsrfGenerator : csrfGenerator != null
				? csrfGenerator : null;
	}

	private boolean shouldRerun40x(Instant previous, int statusCode, boolean shouldSkipSecurity){
		if(HttpStatus.SC_UNAUTHORIZED != statusCode && HttpStatus.SC_FORBIDDEN != statusCode || shouldSkipSecurity){
			return false;
		}
		return refreshableSignatureGenerator != null && !refreshableSignatureGenerator.refresh().isBefore(previous)
				|| refreshableCsrfGenerator != null && !refreshableCsrfGenerator.refresh().isBefore(previous)
				|| refreshableApiKeySupplier != null && !refreshableApiKeySupplier.refresh().isBefore(previous);
	}

	@Override
	public void shutdown(){
		try{
			httpClient.close();
		}catch(IOException e){
			throw new RuntimeException(e);
		}
	}

	@SuppressWarnings("unused")
	private static void forceAbortRequestUnchecked(HttpRequestBase internalHttpRequest){
		try{
			internalHttpRequest.abort();
		}catch(Exception e){
			logger.error("aborting internal http request failed", e);
		}
	}

	@Override
	public StandardDatarouterHttpClient addDtoToPayload(DatarouterHttpRequest request, Object dto, String dtoType){
		String serializedDto = jsonSerializer.serialize(dto);
		String dtoTypeNullSafe = dtoType;
		if(dtoType == null){
			if(dto instanceof Iterable){
				Iterable dtos = (Iterable)dto;
				dtoTypeNullSafe = dtos.iterator().hasNext() ? dtos.iterator().next().getClass().getCanonicalName() : "";
			}else{
				dtoTypeNullSafe = dto.getClass().getCanonicalName();
			}
		}
		DatarouterHttpClientConfig requestConfig = request.getRequestConfig(config);
		Map params = new HashMap<>();
		params.put(requestConfig.getDtoParameterName(), serializedDto);
		params.put(requestConfig.getDtoTypeParameterName(), dtoTypeNullSafe);
		request.addPostParams(params);
		return this;
	}

	@Override
	public StandardDatarouterHttpClient setEntityDto(DatarouterHttpRequest request, Object dto){
		String serializedDto = jsonSerializer.serialize(dto);
		request.setEntity(serializedDto, ContentType.APPLICATION_JSON);
		return this;
	}

	@Override
	public PoolStats getPoolStats(){
		return connectionManager.getTotalStats();
	}

	@Override
	public CloseableHttpClient getApacheHttpClient(){
		return httpClient;
	}

	@Override
	public JsonSerializer getJsonSerializer(){
		return jsonSerializer;
	}

	@Override
	public void initUrlPrefix(BaseEndpoint endpoint){
		endpoint.setUrlPrefix(urlPrefix.get());
	}

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy