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

com.jkoolcloud.tnt4j.streams.inputs.RestStream Maven / Gradle / Ivy

/*
 * Copyright 2014-2023 JKOOL, LLC.
 *
 * 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 com.jkoolcloud.tnt4j.streams.inputs;

import java.io.IOException;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Semaphore;

import org.apache.commons.lang3.StringUtils;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.classic.methods.HttpUriRequest;
import org.apache.hc.client5.http.impl.classic.BasicHttpClientResponseHandler;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.NameValuePair;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.http.message.BasicNameValuePair;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.apache.hc.core5.net.URIBuilder;
import org.quartz.JobBuilder;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;

import com.jkoolcloud.tnt4j.core.OpLevel;
import com.jkoolcloud.tnt4j.sink.EventSink;
import com.jkoolcloud.tnt4j.streams.configure.WsStreamProperties;
import com.jkoolcloud.tnt4j.streams.scenario.WsRequest;
import com.jkoolcloud.tnt4j.streams.scenario.WsResponse;
import com.jkoolcloud.tnt4j.streams.scenario.WsScenarioStep;
import com.jkoolcloud.tnt4j.streams.utils.LoggerUtils;
import com.jkoolcloud.tnt4j.streams.utils.StreamsResources;
import com.jkoolcloud.tnt4j.streams.utils.Utils;
import com.jkoolcloud.tnt4j.streams.utils.WsStreamConstants;

/**
 * Implements a scheduled JAX-RS service call activity stream, where each call response is assumed to represent a single
 * activity or event which should be recorded.
 * 

* Service call is performed by invoking * {@link org.apache.hc.client5.http.classic.HttpClient#execute(org.apache.hc.core5.http.ClassicHttpRequest, org.apache.hc.core5.http.protocol.HttpContext, org.apache.hc.core5.http.io.HttpClientResponseHandler)} * with GET or POST method request depending on scenario step configuration parameter 'method'. Default method is GET. * Stream uses {@link org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager} to handle connections pool * used by HTTP client. *

* This activity stream requires parsers that can support {@link String} data to parse * {@link com.jkoolcloud.tnt4j.streams.scenario.WsResponse#getData()} provided string. *

* This activity stream supports the following configuration properties (in addition to those supported by * {@link AbstractHttpStream}): *

    *
  • MaxTotalPoolConnections - defines the maximum number of total open connections in the HTTP connections pool. * Default value - {@code 5}. (Optional)
  • *
  • DefaultMaxPerRouteConnections - defines the maximum number of concurrent connections per HTTP route. Default * value - {@code 2}. (Optional)
  • *
* * @version $Revision: 3 $ * * @see com.jkoolcloud.tnt4j.streams.parsers.ActivityParser#isDataClassSupported(Object) * @see org.apache.hc.client5.http.classic.HttpClient#execute(org.apache.hc.core5.http.ClassicHttpRequest, * org.apache.hc.core5.http.protocol.HttpContext, org.apache.hc.core5.http.io.HttpClientResponseHandler) * org.apache.hc.core5.http.protocol.HttpContext) */ public class RestStream extends AbstractHttpStream { private static final EventSink LOGGER = LoggerUtils.getLoggerSink(RestStream.class); private static final String REQ_HEAD_PARAM_PREFIX = "H:"; // NON-NLS private static final String REQ_HEAD_PARAM_AUTH = REQ_HEAD_PARAM_PREFIX + HttpHeaders.AUTHORIZATION; private static final BasicHttpClientResponseHandler respHandler = new BasicHttpClientResponseHandler(); private int maxTotalPoolConnections = 5; private int defaultMaxPerRouteConnections = 2; /** * HTTP client instance used to execute JAX-RS calls. */ protected CloseableHttpClient client; /** * Constructs an empty RestStream. Requires configuration settings to set input stream source. */ public RestStream() { super(); } @Override protected EventSink logger() { return LOGGER; } @Override public void setProperty(String name, String value) { super.setProperty(name, value); if (WsStreamProperties.PROP_MAX_TOTAL_POOL_CONNECTIONS.equalsIgnoreCase(name)) { maxTotalPoolConnections = Integer.parseInt(value); } else if (WsStreamProperties.PROP_DEFAULT_MAX_PER_ROUTE_CONNECTIONS.equalsIgnoreCase(name)) { defaultMaxPerRouteConnections = Integer.parseInt(value); } } @Override public Object getProperty(String name) { if (WsStreamProperties.PROP_MAX_TOTAL_POOL_CONNECTIONS.equalsIgnoreCase(name)) { return maxTotalPoolConnections; } if (WsStreamProperties.PROP_DEFAULT_MAX_PER_ROUTE_CONNECTIONS.equalsIgnoreCase(name)) { return defaultMaxPerRouteConnections; } return super.getProperty(name); } @Override protected JobDetail buildJob(String group, String jobId, JobDataMap jobAttrs) { return JobBuilder.newJob(RestCallJob.class).withIdentity(jobId, group).usingJobData(jobAttrs).build(); } @Override protected void initialize() throws Exception { PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(); cm.setMaxTotal(maxTotalPoolConnections); cm.setDefaultMaxPerRoute(defaultMaxPerRouteConnections); // cm.setValidateAfterInactivity((int) TimeUnit.SECONDS.toMillis(4 * 60)); client = HttpClients.custom().setConnectionManager(cm).build(); super.initialize(); } @Override protected void cleanup() { super.cleanup(); Utils.close(client); } /** * Performs JAX-RS service call over HTTP GET method request. * * @param client * HTTP client instance to execute request * @param req * request instance * @return service response string * * @throws Exception * if exception occurs while performing JAX-RS service call */ public static String executeGET(CloseableHttpClient client, WsRequest req) throws Exception { return executeGET(client, req.getData(), req.getParameters()); } /** * Performs JAX-RS service call over HTTP GET method request. * * @param client * HTTP client instance to execute request * @param uriStr * JAX-RS service URI * @param reqParams * request parameters map * @return service response string * * @throws Exception * if exception occurs while performing JAX-RS service call */ public static String executeGET(CloseableHttpClient client, String uriStr, Map reqParams) throws Exception { if (StringUtils.isEmpty(uriStr)) { LOGGER.log(OpLevel.DEBUG, StreamsResources.getBundle(WsStreamConstants.RESOURCE_BUNDLE_NAME), "RestStream.cant.execute.get.request", uriStr); return null; } LOGGER.log(OpLevel.INFO, StreamsResources.getBundle(WsStreamConstants.RESOURCE_BUNDLE_NAME), "RestStream.invoking.get.request", uriStr); HttpGet get = new HttpGet(uriStr); String respStr = executeRequest(client, get, reqParams); LOGGER.log(OpLevel.INFO, StreamsResources.getBundle(WsStreamConstants.RESOURCE_BUNDLE_NAME), "RestStream.received.response", uriStr, respStr); return respStr; } /** * Performs JAX-RS service call over HTTP POST method request. * * @param client * HTTP client instance to execute request * @param req * request instance * @return service response string * * @throws Exception * if exception occurs while performing JAX-RS service call */ public static String executePOST(CloseableHttpClient client, WsRequest req) throws Exception { return executePOST(client, req.getParameterStringValue(REQ_URL_PARAM), req.getData(), req.getParameters()); } /** * Performs JAX-RS service call over HTTP POST method request. * * @param client * HTTP client instance to execute request * @param uriStr * JAX-RS service URI * @param reqData * request data * @param reqParams * request parameters map * @return service response string * * @throws Exception * if exception occurs while performing JAX-RS service call */ public static String executePOST(CloseableHttpClient client, String uriStr, String reqData, Map reqParams) throws Exception { if (StringUtils.isEmpty(uriStr)) { LOGGER.log(OpLevel.DEBUG, StreamsResources.getBundle(WsStreamConstants.RESOURCE_BUNDLE_NAME), "RestStream.cant.execute.post.request", uriStr); return null; } if (StringUtils.isEmpty(reqData)) { return executeGET(client, uriStr, reqParams); } LOGGER.log(OpLevel.INFO, StreamsResources.getBundle(WsStreamConstants.RESOURCE_BUNDLE_NAME), "RestStream.invoking.post.request", uriStr, reqData); HttpPost post = new HttpPost(uriStr); // here instead of JSON you can also have XML StringEntity reqEntity = new StringEntity(reqData, ContentType.APPLICATION_JSON); post.setEntity(reqEntity); String respStr = executeRequest(client, post, reqParams); LOGGER.log(OpLevel.INFO, StreamsResources.getBundle(WsStreamConstants.RESOURCE_BUNDLE_NAME), "RestStream.received.response", uriStr, respStr); return respStr; } private static String executeRequest(CloseableHttpClient client, HttpUriRequest req, Map reqParams) throws Exception { if (client == null) { throw new IllegalArgumentException( StreamsResources.getString(WsStreamConstants.RESOURCE_BUNDLE_NAME, "RestStream.client.null")); } CloseableHttpResponse response = null; try { HttpContext ctx = HttpClientContext.create(); if (reqParams != null) { int idx = 0; for (Map.Entry pe : reqParams.entrySet()) { if (pe.getKey().startsWith(REQ_HEAD_PARAM_PREFIX) && !pe.getValue().isTransient()) { String pName = pe.getKey().substring(REQ_HEAD_PARAM_PREFIX.length()); if (StringUtils.isEmpty(pName)) { throw new IllegalArgumentException(StreamsResources.getStringFormatted( WsStreamConstants.RESOURCE_BUNDLE_NAME, "RestStream.header.param.null", idx)); } req.addHeader(pName, pe.getValue().getStringValue()); } idx++; } } return client.execute(req, ctx, respHandler); } finally { Utils.close(response); } } /** * {@inheritDoc} * * @see #makeCredentialsParam(String, String) */ @Override protected WsRequest fillInRequest(WsRequest req) throws VoidRequestException { if (req.getParameter(REQ_HEAD_PARAM_AUTH) == null) { WsRequest.Parameter hAuthParam = makeCredentialsParam(req.getScenarioStep().getUsername(), req.getScenarioStep().getPassword()); if (hAuthParam != null) { req.addParameter(hAuthParam); } } return super.fillInRequest(req); } /** * Creates request parameter of HTTP message header {@code "Authorization"} for basic authentication. * * @param username * user name used to perform request if service authentication is needed, or {@code null} if no * authentication * @param password * password used to perform request if service authentication is needed, or {@code null} if no * authentication * @return request parameter instance containing basic authentication value, or {@code null} if no proper * credentials provided */ protected static WsRequest.Parameter makeCredentialsParam(String username, String password) { if (StringUtils.isNotEmpty(username)) { String credentialsStr = username + ":" + decPassword(password); // NON-NLS String encoding = Utils.base64EncodeStr(credentialsStr.getBytes()); return new WsRequest.Parameter(REQ_HEAD_PARAM_AUTH, "Basic " + encoding); // NON-NLS); } return null; } /** * {@inheritDoc} * * @see #uriForGET(com.jkoolcloud.tnt4j.streams.scenario.WsRequest) */ @Override protected WsRequest fillInRequest(WsRequest req, String url) throws VoidRequestException { String reqData = req.getData(); if (StringUtils.isEmpty(reqData)) { req.setData(url); } WsRequest fReq = fillInRequest(req); try { String uriStr = uriForGET(fReq); fReq.setData(uriStr); } catch (URISyntaxException exc) { throw new VoidRequestException(StreamsResources.getStringFormatted(WsStreamConstants.RESOURCE_BUNDLE_NAME, "RestStream.invalid.request.uri"), exc); } catch (VoidRequestException exc) { throw exc; } return fReq; } /** * Scheduler job to execute JAX-RS call. */ public static class RestCallJob extends CallJob { /** * Constructs a new RestCallJob. */ public RestCallJob() { } /** * Executes JAX-RS call as HTTP POST request. * * @param scenarioStep * scenario step to execute * @param stream * stream instance to execute HTTP POST */ protected void runPOST(WsScenarioStep scenarioStep, RestStream stream) { if (!scenarioStep.isEmpty()) { String respStr; Semaphore acquiredSemaphore; WsRequest processedRequest; for (WsRequest request : scenarioStep.requestsArray()) { if (stream.isShotDown()) { return; } if (stream.isDropRecurring(request)) { continue; } respStr = null; acquiredSemaphore = null; processedRequest = null; try { acquiredSemaphore = stream.acquireSemaphore(request); request.addParameter(REQ_URL_PARAM, scenarioStep.getUrlStr(), true); processedRequest = stream.fillInRequest(request); respStr = stream.executePOST(stream.client, processedRequest); } catch (VoidRequestException exc) { stream.logger().log(OpLevel.INFO, StreamsResources.getBundle(WsStreamConstants.RESOURCE_BUNDLE_NAME), "AbstractWsStream.void.request", request.getId(), exc.getMessage()); } catch (IOException exc) { stream.logger().log(OpLevel.WARNING, StreamsResources.getBundle(WsStreamConstants.RESOURCE_BUNDLE_NAME), "RestStream.execute.exception", stream.getName(), processedRequest.getId(), exc.getMessage()); } catch (Throwable exc) { Utils.logThrowable(stream.logger(), OpLevel.ERROR, StreamsResources.getBundle(WsStreamConstants.RESOURCE_BUNDLE_NAME), "RestStream.execute.exception", stream.getName(), processedRequest.getId(), exc); } if (StringUtils.isNotEmpty(respStr)) { stream.addInputToBuffer(new WsResponse<>(respStr, processedRequest)); } else { stream.requestFailed(processedRequest); stream.releaseSemaphore(acquiredSemaphore, scenarioStep.getName(), request); } } } } /** * Executes JAX-RS call as HTTP GET request. * * @param scenarioStep * scenario step to execute * @param stream * stream instance to execute HTTP GET */ protected void runGET(WsScenarioStep scenarioStep, RestStream stream) { if (scenarioStep.isEmpty()) { scenarioStep.addRequest(null, scenarioStep.getUrlStr()); } String respStr; Semaphore acquiredSemaphore; WsRequest processedRequest; for (WsRequest request : scenarioStep.requestsArray()) { if (stream.isShotDown()) { return; } if (stream.isDropRecurring(request)) { continue; } respStr = null; acquiredSemaphore = null; processedRequest = null; try { acquiredSemaphore = stream.acquireSemaphore(request); processedRequest = stream.fillInRequest(request, scenarioStep.getUrlStr()); respStr = stream.executeGET(stream.client, processedRequest); } catch (VoidRequestException exc) { stream.logger().log(OpLevel.INFO, StreamsResources.getBundle(WsStreamConstants.RESOURCE_BUNDLE_NAME), "AbstractWsStream.void.request", request.getId(), exc.getMessage()); } catch (IOException exc) { stream.logger().log(OpLevel.WARNING, StreamsResources.getBundle(WsStreamConstants.RESOURCE_BUNDLE_NAME), "RestStream.execute.exception", stream.getName(), processedRequest.getId(), exc.getMessage()); } catch (Throwable exc) { Utils.logThrowable(stream.logger(), OpLevel.ERROR, StreamsResources.getBundle(WsStreamConstants.RESOURCE_BUNDLE_NAME), "RestStream.execute.exception", stream.getName(), processedRequest.getId(), exc); } if (StringUtils.isNotEmpty(respStr)) { stream.addInputToBuffer(new WsResponse<>(respStr, processedRequest)); } else { stream.requestFailed(processedRequest); stream.releaseSemaphore(acquiredSemaphore, scenarioStep.getName(), request); } } } @Override public void executeCalls(JobDataMap dataMap) { RestStream stream = (RestStream) dataMap.get(JOB_PROP_STREAM_KEY); WsScenarioStep scenarioStep = (WsScenarioStep) dataMap.get(JOB_PROP_SCENARIO_STEP_KEY); String reqMethod = scenarioStep.getMethod(); if (StringUtils.isEmpty(reqMethod)) { reqMethod = ReqMethod.GET.name(); } if (ReqMethod.POST.name().equalsIgnoreCase(reqMethod)) { runPOST(scenarioStep, stream); } else if (ReqMethod.GET.name().equalsIgnoreCase(reqMethod)) { runGET(scenarioStep, stream); } } } /** * Appends JAX-RS endpoint URL with query parameter values from request data. * * @param request * request data package * @return URL string appended with query parameter values * * @throws URISyntaxException * if request defines invalid URI * @throws VoidRequestException * if request can't be build from request context data or built URL is meaningless */ protected String uriForGET(WsRequest request) throws URISyntaxException, VoidRequestException { String uri = request.getData(); URIBuilder uriBuilder = new URIBuilder(uri); List params = makeParamsList(request); uriBuilder.addParameters(params); return uriBuilder.toString(); } /** * Builds list of request parameters by filling in values from request context data. * * @param request * request data package * @return list of request parameters */ protected List makeParamsList(WsRequest request) { List params = new ArrayList<>(request.getParameters().size()); for (Map.Entry param : request.getParameters().entrySet()) { if (param.getValue().isTransient() || param.getKey().startsWith(REQ_HEAD_PARAM_PREFIX)) { continue; } BasicNameValuePair httpParam = makeParam(param); if (httpParam != null) { params.add(httpParam); } } return params; } /** * Builds HTTP parameter from request parameter data and request context data. * * @param param * request parameter instance * @return HTTP parameter instance, or {@code null} if parameter can't be built */ protected BasicNameValuePair makeParam(Map.Entry param) { return new BasicNameValuePair(param.getKey(), param.getValue().getStringValue()); } /** * Request method types enumeration. */ protected enum ReqMethod { /** * Request method GET. */ GET, /** * Request method POST. */ POST, } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy