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

io.telicent.jena.graphql.fuseki.ActionGraphQL Maven / Gradle / Ivy

/**
 * Copyright (C) Telicent Ltd
 *
 * 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.telicent.jena.graphql.fuseki;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Locale;
import java.util.Objects;

import graphql.ExecutionResult;
import io.telicent.jena.graphql.execution.GraphQLOverDatasetExecutor;
import io.telicent.jena.graphql.server.model.GraphQLOverHttp;
import io.telicent.jena.graphql.server.model.GraphQLRequest;
import org.apache.commons.lang3.StringUtils;
import org.apache.jena.atlas.logging.FmtLog;
import org.apache.jena.fuseki.servlets.ActionLib;
import org.apache.jena.fuseki.servlets.ActionService;
import org.apache.jena.fuseki.servlets.HttpAction;
import org.apache.jena.fuseki.servlets.ServletOps;
import org.apache.jena.riot.WebContent;
import org.apache.jena.riot.web.HttpNames;
import org.apache.jena.sparql.core.DatasetGraph;

/**
 * A Fuseki action that evaluates GraphQL Requests using a configured Jena GraphQL {@link GraphQLOverDatasetExecutor}
 */
public class ActionGraphQL extends ActionService {

    /**
     * Function that extracts a request parameter from a Fuseki HTTP Action
     *
     * @param req   Fuseki HTTP Action
     * @param param Request parameter
     * @return Request parameter value
     */
    protected static String getRequestParameter(HttpAction req, String param) {
        return req.getRequestParameter(param);
    }

    /**
     * Function that extracts the request body from a Fuseki HTTP Action
     *
     * @param req Fuseki HTTP Action
     * @return Request body
     */
    protected static InputStream getRequestBody(HttpAction req) {
        try {
            return req.getRequestInputStream();
        } catch (IOException e) {
            throw new RuntimeException("Failed to parse POST request body as a valid GraphQL Request", e);
        }
    }

    private final GraphQLOverDatasetExecutor executor;

    /**
     * Creates a new GraphQL Action
     *
     * @param executor GraphQL Executor to use
     */
    public ActionGraphQL(GraphQLOverDatasetExecutor executor) {
        this.executor = Objects.requireNonNull(executor, "GraphQL Executor cannot be null");
    }

    @Override
    public void execGet(HttpAction action) {
        this.executeLifecycle(action);
    }

    @Override
    public void execPost(HttpAction action) {
        this.executeLifecycle(action);
    }

    @Override
    public void execOptions(HttpAction action) {
        ActionLib.doOptionsGetPost(action);
        ServletOps.success(action);
    }

    @Override
    public final void validate(HttpAction httpAction) {
        String method = httpAction.getRequestMethod().toUpperCase(Locale.ROOT);

        if (StringUtils.equals(method, HttpNames.METHOD_GET)) {
            if (StringUtils.isBlank(httpAction.getRequestParameter(GraphQLOverHttp.PARAMETER_QUERY))) {
                ServletOps.errorBadRequest(
                        "GET requests to GraphQL endpoints MUST have a non-empty query parameter");
            }
        } else {
            // Allow only Content-Type: application/json if Content-Type is present, if missing we assume JSON body
            // and will fail parsing in the execute() implementation if it isn't valid JSON
            if (StringUtils.isNotBlank(httpAction.getRequestContentType())
                    && !StringUtils.startsWith(httpAction.getRequestContentType(),
                                               WebContent.contentTypeJSON)) {
                ServletOps.errorBadRequest(
                        "POST requests to GraphQL endpoints MUST use Content-Type: application/json");
            }
        }
    }

    @Override
    public final void execute(HttpAction httpAction) {
        // Parse in the GraphQL Request which may have arrived as either a GET or a POST
        String method = httpAction.getRequestMethod().toUpperCase(Locale.ROOT);

        GraphQLRequest request = null;
        try {
            request = GraphQLOverHttp.parseRequest(httpAction, method, ActionGraphQL::getRequestParameter,
                                                   ActionGraphQL::getRequestBody);
        } catch (Throwable e) {
            ServletOps.errorBadRequest(e.getMessage());
        }

        httpAction.beginRead();
        try {
            DatasetGraph dsg = httpAction.getActiveDSG();
            dsg = prepare(httpAction, request, dsg);

            FmtLog.info(httpAction.log, "[%d] GraphQL Query = \n%s", httpAction.id, request.getQuery());
            ExecutionResult result = this.executor.execute(dsg, request);

            httpAction.setResponseHeader(HttpNames.hContentType, GraphQLOverHttp.CONTENT_TYPE_GRAPHQL_RESPONSE_JSON);
            httpAction.setResponseStatus(GraphQLOverHttp.selectHttpStatus(result));
            try (OutputStream output = httpAction.getResponseOutputStream()) {
                GraphQLOverHttp.write(result, output);
            } catch (IOException e) {
                ServletOps.warning(httpAction, "Failed to serialize GraphQL Results", e);
            }
        } finally {
            httpAction.endRead();
        }
    }

    /**
     * Performs any additional preparation required before executing the GraphQL Request
     * 

* This is intended as an extension point for developers, for example you might want to inject some additional * information/configuration into the GraphQL Request via extensions or modify the {@link DatasetGraph} instance to * be used. *

*

* The default implementation here does nothing and merely returns the dataset unchanged. *

* * @param action The HTTP Action for this request * @param request The parsed GraphQL Request * @param dsg The DatasetGraph for this request * @return The DatasetGraph to execute the GraphQL Request against */ protected DatasetGraph prepare(HttpAction action, GraphQLRequest request, DatasetGraph dsg) { return dsg; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy