Enterprise Java

Upload Files With GraphQL in Java

File uploads are a common feature in modern applications, and GraphQL can handle them with some customization. GraphQL doesn’t natively support file uploads out of the box, and managing file uploads in a GraphQL API involves a few additional steps compared to traditional REST APIs. This article will guide us through creating a Spring Boot application that enables file uploads via GraphQL.

1. Introduction to File Upload with GraphQL

Standard GraphQL doesn’t support file uploads natively. GraphQL typically works with JSON-based queries, and files like images or documents don’t fit into this format. However, we can work around this limitation by using the multipart form data format, which allows files to be transmitted alongside the GraphQL query in a single request.

By defining a custom scalar type called Upload, we can handle file uploads within the GraphQL framework, making it easy to integrate file handling into our GraphQL API.

2. Dependencies

In this section, we’ll add the required dependencies to set up our GraphQL application and enable file uploads. Set up a Spring Boot project which can be done by using Spring Initializr to create a project with the following necessary dependencies:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-graphql</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
            <scope>test</scope>
        </dependency>	
        <dependency>
            <groupId>org.springframework.graphql</groupId>
            <artifactId>spring-graphql-test</artifactId>
            <scope>test</scope>
        </dependency>

2. Define Schema for File Upload

In GraphQL, scalars are used to define custom data types. Here, we define a Upload scalar to represent file uploads. Create a new file called src/main/resources/graphql/mutation.graphqls and add the following content to define the upload operation.

This GraphQL schema defines the structure for a simple file upload mutation.

scalar Upload

type Mutation {
    uploadFile(file: Upload!): String
}

type Query {
    getFile: String
}

In this code, the uploadFile mutation expects one parameter: a file of type Upload. The custom scalar type Upload will be mapped to a MultipartFile, allowing us to process the file upload.

3. Handle Upload Scalar with Coercing

In the context of GraphQL, coercing refers to the process of converting data between the format used by the GraphQL schema and the format received or sent in the API requests and responses. This is important for custom scalar types or complex data formats.

To handle the Upload scalar, we need to implement a custom coercing class that manages how the MultipartFile object is processed.

import graphql.schema.Coercing;
import graphql.schema.CoercingParseLiteralException;
import graphql.schema.CoercingParseValueException;
import graphql.schema.CoercingSerializeException;
import org.springframework.web.multipart.MultipartFile;


public class UploadCoercing implements Coercing<MultipartFile, MultipartFile> {

    @Override
    public MultipartFile serialize(Object dataFetcherResult) throws CoercingSerializeException {
        throw new CoercingSerializeException("Upload is an input-only type");
    }

    @Override
    public MultipartFile parseValue(Object input) throws CoercingParseValueException {
        if (input instanceof MultipartFile) {
            return (MultipartFile) input;
        }
        throw new CoercingParseValueException(
            String.format("Expected a 'MultipartFile' like object but was '%s'.", input != null ? input.getClass() : null)
        );
    }

    @Override
    public MultipartFile parseLiteral(Object input) throws CoercingParseLiteralException {
        throw new CoercingParseLiteralException("Parsing literal of 'MultipartFile' is not supported");
    }
}

The UploadCoercing class defines how the Upload scalar is interpreted. The parseValue method ensures that the input is of type MultipartFile, and it throws exceptions for unsupported operations like serialization and parsing literals. This coercing logic is essential to safely process file uploads and prevent unsupported operations.

4. Handling Multipart Requests

This section contains the GraphqlMultipartHandler class, which processes multipart form-data requests, extracts file uploads, and maps them to GraphQL query variables.

public class GraphqlMultipartHandler {

    // A handler to process GraphQL requests
    private final WebGraphQlHandler graphQlHandler;

    // ObjectMapper used to convert between JSON and Java objects
    private final ObjectMapper objectMapper;

    // Constructor that initializes the handler and object mapper with null checks
    public GraphqlMultipartHandler(WebGraphQlHandler graphQlHandler, ObjectMapper objectMapper) {
        Assert.notNull(graphQlHandler, "WebGraphQlHandler is required");
        Assert.notNull(objectMapper, "ObjectMapper is required");
        this.graphQlHandler = graphQlHandler;
        this.objectMapper = objectMapper;
    }

    // Supported media types for the GraphQL response (GraphQL and JSON)
    public static final List<MediaType> SUPPORTED_RESPONSE_MEDIA_TYPES
            = Arrays.asList(MediaType.APPLICATION_GRAPHQL, MediaType.APPLICATION_JSON);

    private static final Log logger = LogFactory.getLog(GraphQlHttpHandler.class);

    private final IdGenerator idGenerator = new AlternativeJdkIdGenerator();

    // Main method to handle the incoming multipart GraphQL request
    public ServerResponse handleRequest(ServerRequest serverRequest) throws ServletException {
        // Read the 'operations' parameter from the request
        Optional<String> operation = serverRequest.param("operations");

        // Read the 'map' parameter which maps files to query variables
        Optional<String> mapParam = serverRequest.param("map");

        // Parse the 'operations' JSON into a Map
        Map<String, Object> inputQuery = readJson(operation, new TypeReference<>() {});

        // Extract query variables from the input query
        final Map<String, Object> queryVariables;
        if (inputQuery.containsKey("variables")) {
            queryVariables = (Map<String, Object>) inputQuery.get("variables");
        } else {
            queryVariables = new HashMap<>();
        }

        // Handle extensions if present in the request
        Map<String, Object> extensions = new HashMap<>();
        if (inputQuery.containsKey("extensions")) {
            extensions = (Map<String, Object>) inputQuery.get("extensions");
        }

        // Read the file parts from the multipart body
        Map<String, MultipartFile> fileParams = readMultipartBody(serverRequest);

        // Parse the 'map' JSON to associate files with variables
        Map<String, List<String>> fileMapInput = readJson(mapParam, new TypeReference<>() {});

        // Map each file to its corresponding variable path in the GraphQL query
        fileMapInput.forEach((String fileKey, List<String> objectPaths) -> {
            MultipartFile file = fileParams.get(fileKey);
            if (file != null) {
                objectPaths.forEach((String objectPath) -> {
                    MultipartVariableMapper.mapVariable(
                            objectPath,
                            queryVariables,
                            file
                    );
                });
            }
        });

        // Extract the actual GraphQL query and operation name
        String query = (String) inputQuery.get("query");
        String opName = (String) inputQuery.get("operationName");

        // Remote address and cookies (if needed) for the request
        InetSocketAddress remoteAddress = null;
        MultiValueMap<String, HttpCookie> cookies = null;

        // Build the request body including query, variables, and extensions
        Map<String, Object> body = new HashMap<>();
        body.put("query", query);
        body.put("operationName", StringUtils.hasText(opName) ? opName : "");
        body.put("variables", queryVariables);
        body.put("extensions", extensions);

        // Create a WebGraphQlRequest that wraps all the information required for processing
        WebGraphQlRequest graphQlRequest = new WebGraphQlRequest(serverRequest.uri(),
                 serverRequest.headers().asHttpHeaders(), cookies,
                 remoteAddress, queryVariables,
                 body, this.idGenerator.generateId().toString(), LocaleContextHolder.getLocale()
        );

        if (logger.isDebugEnabled()) {
            logger.debug("Executing: " + graphQlRequest);
        }

        // Execute the request and return the server response as an asynchronous result
        Mono<ServerResponse> responseMono = this.graphQlHandler.handleRequest(graphQlRequest)
                .map(response -> {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Execution complete");
                    }
                    // Build the response with appropriate headers and content type
                    ServerResponse.BodyBuilder builder = ServerResponse.ok();
                    builder.headers(headers -> headers.putAll(response.getResponseHeaders()));
                    builder.contentType(selectResponseMediaType(serverRequest));
                    return builder.body(response.toMap());
                });

        // Return the async server response
        return ServerResponse.async(responseMono);
    }
}

The GraphqlMultipartHandler class is responsible for handling multipart file uploads in GraphQL requests. It integrates a WebGraphQlHandler for processing the core GraphQL logic and an ObjectMapper for JSON parsing. The main functionality involves reading the incoming request, extracting the GraphQL query and associated variables, mapping file uploads to the appropriate GraphQL query parameters, and finally, executing the request and returning the appropriate response.

The handler supports both GraphQL and JSON media types and is designed to handle multipart forms, mapping files to GraphQL variables via a map parameter. The handleRequest method reads the file and query from the multipart body, logs the request, and processes it asynchronously. It parses the operation and maps parameters from the request, ensuring that uploaded files are correctly assigned to the variables in the query. The response is then built using appropriate headers and media types.

The class also includes utility methods like readJson for parsing JSON data and readMultipartBody for handling the file content. The full source code of this class can be downloaded from the link provided at the end of this article.

5. File Upload Data Fetcher

In this part of the article, we create a DataFetcher to manage the file upload mutation. The GraphQLFileUploader works with the FileStorageService to save the file and return the file path after a successful upload.

import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;


@Component
public class GraphQLFileUploader implements DataFetcher<String> {
    private final FileStorageService fileStorageService;

    public GraphQLFileUploader(FileStorageService fileStorageService) {
        this.fileStorageService = fileStorageService;
    }

    @Override
    public String get(DataFetchingEnvironment environment) {
        MultipartFile file = environment.getArgument("file");
        
        String storedFilePath = fileStorageService.store(file);
        return String.format("File stored at: %s", storedFilePath);
    }
}

The GraphQLFileUploader class is a Spring component that handles file uploads in a GraphQL API by implementing the DataFetcher interface. It retrieves the file from the DataFetchingEnvironment during a GraphQL mutation and uses the FileStorageService to store the file. In the get() method, the file is extracted from the mutation request, passed to the FileStorageService for saving, and a confirmation message with the file’s storage path is returned.

5.1 File Storage Service

In this section, we define the FileStorageService class, which handles the storage of uploaded files.

import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Objects;

@Service
public class FileStorageService {

    private final Path rootLocation = Paths.get("uploads");

    public FileStorageService() {
        try {
            Files.createDirectories(rootLocation);
        } catch (IOException e) {
            throw new RuntimeException("Failed to initialize the storage location.", e);
        }
    }

    public String store(MultipartFile file) {
        try {
            if (file.isEmpty()) {
                throw new RuntimeException("Failed to store empty file.");
            }
            Path destination = rootLocation.resolve(
                            Paths.get(Objects.requireNonNull(file.getOriginalFilename())))
                    .normalize().toAbsolutePath();
            file.transferTo(destination);
            return String.format("File uploaded successfully: %s", destination);
        } catch (IOException e) {
            throw new RuntimeException("Failed to store file.", e);
        }
    }
}

The FileStorageService class handles file storage in the application. It initializes with an uploads directory and ensures it exists. If directory creation fails, an exception is thrown. The store method saves the MultipartFile to the specified directory (a directory named uploads, which is located relative to the application’s working directory).

6. GraphQL Configuration

This section covers configuring the GraphQL server to recognize the Upload scalar and route file uploads properly.

import com.fasterxml.jackson.databind.ObjectMapper;
import static com.jcg.GraphqlMultipartHandler.SUPPORTED_RESPONSE_MEDIA_TYPES;
import graphql.schema.GraphQLScalarType;
import static graphql.schema.idl.TypeRuntimeWiring.newTypeWiring;
import org.springframework.boot.autoconfigure.graphql.GraphQlProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
import org.springframework.graphql.server.WebGraphQlHandler;
import org.springframework.http.MediaType;
import static org.springframework.http.MediaType.MULTIPART_FORM_DATA;
import org.springframework.web.servlet.function.RequestPredicates;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.RouterFunctions;
import org.springframework.web.servlet.function.ServerResponse;


@Configuration
public class GraphqlConfiguration {

    private final GraphQLFileUploader fileUploadDataFetcher;

    public GraphqlConfiguration(GraphQLFileUploader fileUploadDataFetcher) {
        this.fileUploadDataFetcher = fileUploadDataFetcher;
    }

    @Bean
    public RuntimeWiringConfigurer runtimeWiringConfigurer() {
        return (builder) -> builder
                .type(newTypeWiring("Mutation").dataFetcher("uploadFile", fileUploadDataFetcher))
                .scalar(GraphQLScalarType.newScalar()
                        .name("Upload")
                        .coercing(new UploadCoercing())
                        .build());
    }

    @Bean
    @Order(1)
    public RouterFunction<ServerResponse> graphQlMultipartRouterFunction(
            GraphQlProperties properties,
            WebGraphQlHandler webGraphQlHandler,
            ObjectMapper objectMapper
    ) {
        String path = properties.getPath();
        RouterFunctions.Builder builder = RouterFunctions.route();
        GraphqlMultipartHandler graphqlMultipartHandler = new GraphqlMultipartHandler(webGraphQlHandler, objectMapper);
        builder = builder.POST(path, RequestPredicates.contentType(MULTIPART_FORM_DATA)
                .and(RequestPredicates.accept(SUPPORTED_RESPONSE_MEDIA_TYPES.toArray(MediaType[]::new))), graphqlMultipartHandler::handleRequest);
        return builder.build();
    }
}

The GraphqlConfiguration class configures GraphQL for handling file uploads. The runtimeWiringConfigurer method sets up a data fetcher for the uploadFile mutation and defines a custom Upload scalar type with the UploadCoercing implementation. This method ensures that file upload operations are correctly wired in the GraphQL schema.

The graphQlMultipartRouterFunction method establishes routing for multipart form-data requests. It creates a RouterFunction that directs POST requests with MULTIPART_FORM_DATA content to the GraphqlMultipartHandler, which processes the file uploads. This setup enables efficient handling of file uploads in the GraphQL API.

7. Testing the File Upload API

We can test the file upload API using curl commands. Here’s an example curl request (replace the file path with your actual file location):

curl --location --request POST 'http://localhost:8080/graphql' --form 'operations={"query": "mutation UploadFile($file: Upload!) { uploadFile(file: $file) }", "variables": {"file": null}}' --form 'map={"file": ["variables.file"]}' --form 'file=@"/Users/omozegieaziegbe/Downloads/phoneicon.jpeg"'

The POST request to the GraphQL endpoint is structured to handle file uploads using multipart form-data. The operations form field specifies the GraphQL mutation UploadFile, which requires a file parameter. This field includes the GraphQL query and the variables for the mutation, detailing what operation to perform and which data to use.

The map form field links the uploaded file to the mutation variable variables.file, ensuring the server correctly associates the file with the mutation parameter. The file form field includes the actual file data, provided via a file path. Together, these components enable the file to be sent and processed correctly by the GraphQL server.

Upon a successful file upload POST request, the response will confirm the file was uploaded successfully. Here’s what to expect in the output:

java graphql upload file example output
Java GraphQL File Upload Output: Example of successful file upload response

8. Conclusion

In this article, we explored how to implement file upload functionality in a Java GraphQL application using Spring Boot. We covered handling multipart requests and creating a DataFetcher to manage file uploads. By the end, we demonstrated how to test the implementation using a sample curl request. With these steps, we can incorporate file uploads into our own GraphQL APIs.

9. Download the Source Code

This article focuses on implementing Java GraphQL upload file functionality.

Download
You can download the full source code of this example here: java graphql upload file

Omozegie Aziegbe

Omos holds a Master degree in Information Engineering with Network Management from the Robert Gordon University, Aberdeen. Omos is currently a freelance web/application developer who is currently focused on developing Java enterprise applications with the Jakarta EE framework.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button