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

net.n2oapp.framework.boot.graphql.GraphQlDataProviderEngine Maven / Gradle / Ivy

There is a newer version: 7.28.2
Show newest version
package net.n2oapp.framework.boot.graphql;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import net.n2oapp.criteria.dataset.DataSet;
import net.n2oapp.framework.api.data.MapInvocationEngine;
import net.n2oapp.framework.api.exception.N2oException;
import net.n2oapp.framework.api.metadata.dataprovider.N2oGraphQlDataProvider;
import net.n2oapp.framework.engine.data.QueryUtil;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.web.client.RestClientResponseException;
import org.springframework.web.client.RestTemplate;

import java.io.IOException;
import java.util.*;
import java.util.function.BiFunction;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static net.n2oapp.framework.boot.graphql.GraphQlUtil.escapeJson;
import static net.n2oapp.framework.boot.graphql.GraphQlUtil.toGraphQlString;
import static net.n2oapp.framework.engine.data.QueryUtil.*;
import static org.apache.commons.lang3.ArrayUtils.contains;

/**
 * GraphQL провайдер данных
 */
@Slf4j
public class GraphQlDataProviderEngine implements MapInvocationEngine {

    private static final String RESPONSE_ERROR_KEY = "errors";
    private static final String RESPONSE_ERROR_MESSAGE_KEY = "message";
    private static final String RESPONSE_DATA_KEY = "data";
    private final Pattern variablePattern = Pattern.compile("\\$\\w+");
    private final Pattern placeholderKeyPattern = Pattern.compile("\\$\\$\\w+\\s*:");
    private final Pattern selectKeyPattern = Pattern.compile("\\$\\$\\w+\\W");
    private final Pattern placeholderStringEscapePattern = Pattern.compile("\\$\\$\\$\\w+");

    @Value("${n2o.engine.graphql.endpoint:}")
    private String endpoint;
    @Value("${n2o.engine.graphql.access-token:}")
    private String accessToken;
    @Value("${n2o.engine.graphql.forward-headers:}")
    private String forwardHeaders;
    @Value("${n2o.engine.graphql.forward-cookies:}")
    private String forwardCookies;
    @Value("${n2o.engine.graphql.filter-separator:}")
    private String defaultFilterSeparator;
    @Value("${n2o.engine.graphql.sorting-separator:}")
    private String defaultSortingSeparator;
    @Value("${n2o.engine.graphql.filter-prefix:}")
    private String defaultFilterPrefix;
    @Value("${n2o.engine.graphql.filter-suffix:}")
    private String defaultFilterSuffix;
    @Value("${n2o.engine.graphql.sorting-prefix:}")
    private String defaultSortingPrefix;
    @Value("${n2o.engine.graphql.sorting-suffix:}")
    private String defaultSortingSuffix;

    @Value("${n2o.engine.graphql.data-over-errors:false}")
    private boolean dataOverErrors;

    @Setter
    private RestTemplate restTemplate;
    private ObjectMapper mapper;

    public GraphQlDataProviderEngine(RestTemplate restTemplate, ObjectMapper mapper) {
        this.restTemplate = restTemplate;
        this.mapper = mapper;
    }

    @Override
    public Class getType() {
        return N2oGraphQlDataProvider.class;
    }

    @Override
    public DataSet invoke(N2oGraphQlDataProvider invocation, Map data) {
        return execute(invocation, prepareQuery(invocation, data), data);
    }

    /**
     * Формирование и отправка GraphQl запроса
     *
     * @param invocation Провайдер данных
     * @param query      Строка GraphQl запроса
     * @param data       Входные данные
     * @return Исходящие данные
     */
    private DataSet execute(N2oGraphQlDataProvider invocation, String query, Map data) {
        Map payload = initPayload(invocation, query, data);
        String endpoint = initEndpoint(invocation.getEndpoint());

        HttpHeaders headers = new HttpHeaders();
        copyForwardedHeaders(resolveForwardedHeaders(invocation), headers);
        copyForwardedCookies(resolveForwardedCookies(invocation), headers);
        headers.setContentType(MediaType.APPLICATION_JSON);
        addAuthorization(invocation, headers);
        HttpEntity> entity = new HttpEntity<>(payload, headers);
        try {
            DataSet result = restTemplate.postForObject(endpoint, entity, DataSet.class);
            checkErrors(result, query);
            return result;
        } catch (RestClientResponseException e) {
            try {
                checkErrors(mapper.readValue(e.getResponseBodyAsString(), DataSet.class), query);
            } catch (IOException ex) {
                throw new IllegalStateException(ex);
            }
            throw e;
        }
    }

    /**
     * Проверка наличия RESPONSE_ERROR_KEY в ответе сервера
     *
     * @param response Ответ GraphQl сревера
     * @param query    Строка GraphQl запроса
     */
    private void checkErrors(DataSet response, String query) {
        if (response.containsKey(RESPONSE_ERROR_KEY)) {
            Object data = response.get(RESPONSE_DATA_KEY);
            if (data != null && !((DataSet) data).isEmpty() && dataOverErrors)
                return;

            log.error("Execution error with GraphQL query: " + query);
            throw new N2oGraphQlException(((DataSet) response.getList(RESPONSE_ERROR_KEY).get(0)).getString(RESPONSE_ERROR_MESSAGE_KEY),
                    query, response);
        }
    }

    /**
     * Парсинг и выбор заголовков для пересылки
     *
     * @param invocation Провайдер данных
     */
    private Set resolveForwardedHeaders(N2oGraphQlDataProvider invocation) {
        String headers = invocation.getForwardedHeaders() != null ? invocation.getForwardedHeaders() : forwardHeaders;
        return parseHeadersString(headers);
    }

    /**
     * Парсинг и выбор cookie для пересылки
     *
     * @param invocation Провайдер данных
     */
    private Set resolveForwardedCookies(N2oGraphQlDataProvider invocation) {
        String cookies = invocation.getForwardedCookies() != null ? invocation.getForwardedCookies() : forwardCookies;
        return parseHeadersString(cookies);
    }

    /**
     * Добавление авторизации в хэдер запроса
     *
     * @param invocation Провайдер данных
     * @param headers    Хэдер запроса
     */
    private void addAuthorization(N2oGraphQlDataProvider invocation, HttpHeaders headers) {
        String token = invocation.getAccessToken() != null ?
                invocation.getAccessToken() : accessToken;
        headers.set("Authorization", "Bearer " + token);
    }

    /**
     * Формирование GraphQl запроса
     *
     * @param invocation Провайдер данных
     * @param data       Входные данные
     * @return GraphQl запрос
     */
    private String prepareQuery(N2oGraphQlDataProvider invocation, Map data) {
        if (invocation.getQuery() == null)
            throw new N2oException("Строка GraphQl запроса не задана");
        return resolvePlaceholders(invocation, data);
    }

    /**
     * Инициализация payload для отправки
     *
     * @param invocation Провайдер данных
     * @param query      Строка GraphQl запроса
     * @param data       Входные данные
     * @return Payload
     */
    private Map initPayload(N2oGraphQlDataProvider invocation, String query, Map data) {
        Map payload = new HashMap<>();
        payload.put("query", query);
        payload.put("variables", initVariables(invocation, query, data));
        return payload;
    }

    /**
     * Замена плейсхолдеров во входной строке GraphQl запроса
     *
     * @param invocation Провайдер данных
     * @param data       Входные данные
     * @return Строка GraphQl запроса с плейсхолдерами, замененными данными
     */
    private String resolvePlaceholders(N2oGraphQlDataProvider invocation, Map data) {
        String query = invocation.getQuery();
        Map args = new HashMap<>(data);

        resolveHierarchicalSelect(args);
        query = replaceListPlaceholder(query, "$$select", args.remove("select"), "", QueryUtil::reduceSpace);
        if (args.get("sorting") != null) {
            String prefix = Objects.requireNonNullElse(invocation.getSortingPrefix(), defaultSortingPrefix);
            String suffix = Objects.requireNonNullElse(invocation.getSortingSuffix(), defaultSortingSuffix);
            args.put("sorting", QueryUtil.insertPrefixSuffix((List) args.get("sorting"), prefix, suffix));
            String sortingSeparator = Objects.requireNonNullElse(invocation.getSortingSeparator(), defaultSortingSeparator);
            query = replaceListPlaceholder(query, "$$sorting", args.remove("sorting"),
                    "", (a, b) -> QueryUtil.reduceSeparator(a, b, sortingSeparator));
        }
        if (invocation.getPageMapping() == null)
            query = replacePlaceholder(query, "$$page", args.remove("page"), "1");
        if (invocation.getSizeMapping() == null)
            query = replacePlaceholder(query, "$$size", args.remove("limit"), "10");
        query = replacePlaceholder(query, "$$offset", args.remove("offset"), "0");
        if (args.get("filters") != null) {
            String prefix = Objects.requireNonNullElse(invocation.getFilterPrefix(), defaultFilterPrefix);
            String suffix = Objects.requireNonNullElse(invocation.getFilterSuffix(), defaultFilterSuffix);
            args.put("filters", QueryUtil.insertPrefixSuffix((List) args.get("filters"), prefix, suffix));
            String filterSeparator = Objects.requireNonNullElse(invocation.getFilterSeparator(), defaultFilterSeparator);
            query = replaceListPlaceholder(query, "$$filters", args.remove("filters"),
                    "", (a, b) -> QueryUtil.reduceSeparator(a, b, filterSeparator));
        }

        Set placeholderKeys = extractPlaceholderKeys(query);
        Set escapeStringPlaceholders = extractEscapeStringPlaceholder(query);
        for (Map.Entry entry : args.entrySet()) {
            String placeholder = "$$".concat(entry.getKey());
            String value;
            if (escapeStringPlaceholders.contains(entry.getKey())) {
                placeholder = "$".concat(placeholder);
                value = escapeJson(toGraphQlString(entry.getValue()));
            } else {
                value = placeholderKeys.contains(entry.getKey()) || contains(invocation.getEnums(),entry.getKey()) ?
                        (String) entry.getValue() :
                        toGraphQlString(entry.getValue());
            }
            query = replacePlaceholder(query, placeholder, value, "null");
        }

        log.debug("Execute GraphQL query: " + query);
        return query;
    }

    /**
     * Замена плейсхолдеров в "select"
     *
     * @param args Данные
     */
    private void resolveHierarchicalSelect(Map args) {
        @SuppressWarnings("unchecked")//Всегда приходит в виде списка из select-expression
        List selectExpressions = (List) args.get("select");
        if (selectExpressions == null)
            return;
        List resolvedExpressions = new ArrayList<>();
        for (String selectExpression : selectExpressions) {
            while (selectKeyPattern.matcher(selectExpression).find()) {
                selectExpression = resolveSelectKey(selectExpression, args);
            }
            resolvedExpressions.add(selectExpression);
        }
        args.put("select", resolvedExpressions);
    }

    /**
     * Замена плейсхолдера в select-expression
     *
     * @param selectExpression Выражение
     * @param args             Данные
     * @return Разрезолвленное выражение или исходное при отсутствии в нем плейсхолдеров $$
     */
    private String resolveSelectKey(String selectExpression, Map args) {
        Set selectKeys = extract(selectExpression, selectKeyPattern, (s, m) -> s.substring(m.start() + 2, m.end() - 1));
        Optional selectKey = selectKeys.stream().findFirst();
        if (selectKey.isEmpty())
            return selectExpression;
        if (selectKeys.size() > 1)
            throw new N2oException("Find more than one select key in expression " + selectExpression);

        @SuppressWarnings("unchecked")//Всегда приходит в виде списка из select-expression
        List value = (List) args.remove(selectKey.get());
        if (value == null)
            throw new N2oException(String.format("Value for placeholder %s not found ", "$$" + selectKey.get()));
        return replacePlaceholder(selectExpression, "$$" + selectKey.get(), String.join(" ", value), "");
    }

    /**
     * Инициализация множества переменных GraphQl запроса значениями
     *
     * @param invocation Провайдер данных
     * @param query      Строка GraphQl запроса
     * @param data       Входные данные
     * @return Объект со значениями переменных GraphQl запроса
     */
    private Object initVariables(N2oGraphQlDataProvider invocation, String query, Map data) {
        Set variables = extractVariables(query);
        DataSet result = new DataSet();

        if (invocation.getPageMapping() != null)
            data.put(invocation.getPageMapping(), data.get("page"));
        if (invocation.getSizeMapping() != null)
            data.put(invocation.getSizeMapping(), data.get("limit"));

        for (String variable : variables) {
            if (data.containsKey(variable))
                result.add(variable, data.get(variable));
        }
        return result;
    }

    /**
     * Получение множества переменных из входной строки GraphQl запроса
     *
     * @param query Строка GraphQl запроса
     * @return Множество переменных
     */
    private Set extractVariables(String query) {
        return extract(query, variablePattern, (s, m) -> s.substring(m.start() + 1, m.end()));
    }

    /**
     * Получение множества ключей-плейсхолдеров из входной строки GraphQl запроса
     *
     * @param query Строка GraphQl запроса
     * @return Множество ключей-плейсхолдеров
     */
    private Set extractPlaceholderKeys(String query) {
        return extract(query, placeholderKeyPattern, (s, m) -> s.substring(m.start() + 2, m.end() - 1).trim());
    }

    /**
     * Получение множества экранированных строковых плейсхолдеров
     *
     * @param query Строка GraphQl запроса
     * @return Множество экранированных строковых плейсхолдеров
     */
    private Set extractEscapeStringPlaceholder(String query) {
        return extract(query, placeholderStringEscapePattern, (s, m) -> s.substring(m.start() + 3, m.end()));
    }

    /**
     * Получение множества значений из входной строки GraphQl запроса
     *
     * @param query    Строка GraphQl запроса
     * @param pattern  Паттерн, по которому будут находиться значения
     * @param function Функция, описывающая действия с найденными значениями
     * @return Множество значений
     */
    private Set extract(String query, Pattern pattern, BiFunction function) {
        Set result = new HashSet<>();
        Matcher matcher = pattern.matcher(query);
        while (matcher.find())
            result.add(function.apply(query, matcher));
        return result;
    }

    /**
     * Инициализация эндпоинта
     *
     * @param invocationEndpoint Эндпоинт, заданный в моделе провайдера
     * @return Эндпоинт
     */
    private String initEndpoint(String invocationEndpoint) {
        return invocationEndpoint != null ? invocationEndpoint : endpoint;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy