com.wavemaker.runtime.rest.RestInvocationHandler Maven / Gradle / Ivy
The newest version!
/*******************************************************************************
* Copyright (C) 2022-2023 WaveMaker, Inc.
*
* 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.wavemaker.runtime.rest;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;
import com.wavemaker.commons.MessageResource;
import com.wavemaker.commons.WMRuntimeException;
import com.wavemaker.commons.rest.WmFileSystemResource;
import com.wavemaker.runtime.commons.WMObjectMapper;
import com.wavemaker.runtime.rest.model.HttpRequestData;
import com.wavemaker.runtime.rest.model.HttpResponseDetails;
import com.wavemaker.runtime.rest.model.Message;
import com.wavemaker.runtime.rest.service.RestRuntimeService;
import com.wavemaker.runtime.rest.util.HttpRequestUtils;
import feign.Headers;
import feign.Param;
import feign.QueryMap;
import feign.RequestLine;
public class RestInvocationHandler implements InvocationHandler {
private static final Logger logger = LoggerFactory.getLogger(RestInvocationHandler.class);
private static final Pattern pathParameterPattern = Pattern.compile("\\/\\{(\\w*)\\}");
private static final Pattern queryParameterPattern = Pattern.compile("=\\{(\\w*)\\}");
private static final Pattern headerParameterPattern = Pattern.compile("\\{(\\w*)\\}");
private static final Pattern splitHeaderPattern = Pattern.compile("([^:]+):\\s*(.+)");
private RestRuntimeService restRuntimeService;
private EncodingMode encodingMode;
private String serviceId;
public RestInvocationHandler(String serviceId, RestRuntimeService restRuntimeService, EncodingMode encodingMode) {
this.serviceId = serviceId;
this.restRuntimeService = restRuntimeService;
this.encodingMode = encodingMode;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
HttpRequestData httpRequestData = new HttpRequestData();
Map pathVariablesMap = new HashMap<>();
MultiValueMap queryVariablesMap = new LinkedMultiValueMap<>();
Map headerVariableMap = new HashMap<>();
MultiValueMap formVariableMap = new LinkedMultiValueMap<>();
List queryVariablesList = getQueryVariables(method.getAnnotation(RequestLine.class).value());
List pathVariablesList = getPathVariables(method.getAnnotation(RequestLine.class).value());
List headerPlaceholders = extractHeaderPlaceholders(method.getAnnotation(Headers.class).value());
boolean urlEncodedHeader = isUrlEncodedHeaderPresent(method.getAnnotation(Headers.class).value());
boolean multipartFormDataHeader = isMultipartFormDataHeaderPresent(method.getAnnotation(Headers.class).value());
int position = 0;
for (Annotation[] parameterAnnotation : method.getParameterAnnotations()) {
if (args[position] != null) {
if (parameterAnnotation.length != 0 && parameterAnnotation[0] instanceof Param) {
if (queryVariablesList.contains(((Param) parameterAnnotation[0]).value())) {
queryVariablesMap.add(((Param) parameterAnnotation[0]).value(), args[position].toString());
} else if (headerPlaceholders.contains(((Param) parameterAnnotation[0]).value())) {
headerVariableMap.put(((Param) parameterAnnotation[0]).value(), args[position].toString());
} else if (pathVariablesList.contains(((Param) parameterAnnotation[0]).value())) {
pathVariablesMap.put(((Param) parameterAnnotation[0]).value(), args[position].toString());
} else if (urlEncodedHeader || multipartFormDataHeader) {
formVariableMap.add(((Param) parameterAnnotation[0]).value(), args[position]);
}
} else if (parameterAnnotation.length != 0 && parameterAnnotation[0] instanceof QueryMap) {
queryVariablesMap.addAll((MultiValueMap) args[position]);
} else {
//set as request body
httpRequestData.setRequestBody(new ByteArrayInputStream(WMObjectMapper.getInstance().writeValueAsBytes(args[position])));
httpRequestData.getHttpHeaders().add("content-type", "application/json");
//TODO:file
}
}
position++;
}
httpRequestData.setQueryParametersMap(queryVariablesMap);
httpRequestData.setPathVariablesMap(pathVariablesMap);
if (!formVariableMap.isEmpty()) {
Message formMessage = null;
if (multipartFormDataHeader) {
MultiValueMap convertedMap = convertToMultipartData(formVariableMap);
formMessage = HttpRequestUtils.createMessage(convertedMap, MediaType.MULTIPART_FORM_DATA_VALUE);
} else if (urlEncodedHeader) {
formMessage = HttpRequestUtils.createMessage(formVariableMap, MediaType.APPLICATION_FORM_URLENCODED_VALUE);
}
if (formMessage != null) {
httpRequestData.setRequestBody(formMessage.getInputStream());
httpRequestData.getHttpHeaders().addAll(formMessage.getHttpHeaders());
}
}
logger.debug("constructed request data {}", httpRequestData.getPathVariablesMap());
logger.debug("constructed request query param {}", httpRequestData.getQueryParametersMap());
//Resolving the headers and setting them to httpRequestData
//eg: if Header is like X-RapidAPI-Key: {x_RapidAPI_Key} in this case we will look for x_RapidAPI_Key in variableMap and set its value
Arrays.stream(method.getAnnotation(Headers.class).value())
.map(splitHeaderPattern::matcher)
.filter(Matcher::find)
.forEach(matcher -> {
String headerName = matcher.group(1);
String headerValue = matcher.group(2);
if (headerValue.startsWith("{") && headerValue.endsWith("}")) {
headerValue = headerVariableMap.getOrDefault(headerValue.substring(1, headerValue.length() - 1), headerValue);
}
httpRequestData.getHttpHeaders().addIfAbsent(headerName, headerValue);
});
String[] split = method.getAnnotation(RequestLine.class).value().split(" ");
HttpResponseDetails responseDetails = restRuntimeService.executeRestCall(serviceId,
split[1].contains("?") ? split[1].subSequence(0, split[1].indexOf("?")).toString() : split[1],
split[0],
httpRequestData, RestExecutor.getRequestContextThreadLocal(), encodingMode);
try {
if (method.getReturnType() != void.class) {
if (responseDetails.getStatusCode() >= 200 && responseDetails.getStatusCode() < 300) {
if (method.getGenericReturnType() instanceof ParameterizedType) {
return WMObjectMapper.getInstance().readValue(responseDetails.getBody(),
WMObjectMapper.getInstance().getTypeFactory()
.constructCollectionType((Class extends Collection>) Class.forName(method.getReturnType().getName()),
Class.forName(((ParameterizedType) method.getGenericReturnType()).getActualTypeArguments()[0].getTypeName(),
true, Thread.currentThread().getContextClassLoader())));
}
return WMObjectMapper.getInstance().readValue(responseDetails.getBody(), method.getReturnType());
} else {
logger.error(IOUtils.toString(responseDetails.getBody(), Charset.defaultCharset()));
throw new WMRuntimeException(MessageResource.create("com.wavemaker.runtime.$RestServiceInvocationError"), responseDetails.getStatusCode());
}
}
} catch (IOException e) {
throw new WMRuntimeException(e);
}
return null;
}
private MultiValueMap convertToMultipartData(MultiValueMap formVariableMap) {
MultiValueMap convertedMap = new LinkedMultiValueMap<>();
formVariableMap.forEach((key, value) -> {
if (value != null) {
if (value.get(0) instanceof List) {
List> listValue = (List>) value.get(0);
if (!listValue.isEmpty() && listValue.get(0) instanceof File) {
for (Object item : listValue) {
if (item instanceof File) {
addFileToConvertMap((File) item, convertedMap, key);
}
}
}
} else {
convertedMap.add(key, value.get(0));
}
}
});
return convertedMap;
}
private List extractHeaderPlaceholders(String[] value) {
List headerVariables = new ArrayList();
Arrays.stream(value).forEach(header -> {
Matcher matcher = headerParameterPattern.matcher(header);
while (matcher.find()) {
headerVariables.add(matcher.group(1));
}
});
return headerVariables;
}
private List getPathVariables(String value) {
List arrayList = new LinkedList<>();
Matcher matcher = pathParameterPattern.matcher(value);
while (matcher.find()) {
arrayList.add(matcher.group(1));
}
return arrayList;
}
private List getQueryVariables(String value) {
List queryVariables = new ArrayList();
Matcher matcher = queryParameterPattern.matcher(value);
while (matcher.find()) {
queryVariables.add(matcher.group(1));
}
return queryVariables;
}
private void addFileToConvertMap(File file, MultiValueMap convertedMap, String key) {
WmFileSystemResource fileSystemResource = new WmFileSystemResource(file, MediaType.MULTIPART_FORM_DATA_VALUE);
convertedMap.add(key, fileSystemResource);
}
private boolean isUrlEncodedHeaderPresent(String[] headerList) {
return Arrays.stream(headerList)
.anyMatch(header -> header.contains(MediaType.APPLICATION_FORM_URLENCODED_VALUE));
}
private boolean isMultipartFormDataHeaderPresent(String[] headerList) {
return Arrays.stream(headerList)
.anyMatch(header -> header.contains(MediaType.MULTIPART_FORM_DATA_VALUE));
}
}