Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.xlrit.gears.server.graphql.GraphqlMultipartHandler Maven / Gradle / Ivy
package com.xlrit.gears.server.graphql;
import java.net.URI;
import java.util.*;
import java.util.regex.Pattern;
import jakarta.servlet.ServletException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.graphql.ExecutionGraphQlRequest;
import org.springframework.graphql.server.WebGraphQlHandler;
import org.springframework.graphql.server.WebGraphQlRequest;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.util.AlternativeJdkIdGenerator;
import org.springframework.util.IdGenerator;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.support.AbstractMultipartHttpServletRequest;
import org.springframework.web.server.ServerWebInputException;
import org.springframework.web.servlet.function.ServerRequest;
import org.springframework.web.servlet.function.ServerResponse;
import reactor.core.publisher.Mono;
// from https://github.com/spring-projects/spring-graphql/issues/69
@RequiredArgsConstructor
public class GraphqlMultipartHandler {
private static final Logger LOG = LoggerFactory.getLogger(GraphqlMultipartHandler.class);
public static final List SUPPORTED_RESPONSE_MEDIA_TYPES =
Arrays.asList(MediaType.APPLICATION_GRAPHQL, MediaType.APPLICATION_JSON);
private final WebGraphQlHandler graphQlHandler;
private final ObjectMapper objectMapper;
private final IdGenerator idGenerator = new AlternativeJdkIdGenerator();
public ServerResponse handleRequest(ServerRequest serverRequest) throws ServletException {
Optional operation = serverRequest.param("operations");
Optional mapParam = serverRequest.param("map");
Map inputQuery = readJson(operation, new TypeReference<>() {});
final Map queryVariables;
if (inputQuery.containsKey("variables")) {
//noinspection unchecked
queryVariables = (Map) inputQuery.get("variables");
} else {
queryVariables = new HashMap<>();
}
Map extensions = new HashMap<>();
if (inputQuery.containsKey("extensions")) {
//noinspection unchecked
extensions = (Map) inputQuery.get("extensions");
}
Map fileParams = readMultipartBody(serverRequest);
Map> fileMapInput = readJson(mapParam, new TypeReference<>() {});
fileMapInput.forEach((String fileKey, List objectPaths) -> {
MultipartFile file = fileParams.get(fileKey);
if (file != null) {
objectPaths.forEach((String objectPath) -> {
MultipartVariableMapper.mapVariable(
objectPath,
queryVariables,
file
);
});
}
});
String query = (String) inputQuery.get("query");
String opName = (String) inputQuery.get("operationName");
WebGraphQlRequest graphQlRequest = new MultipartGraphQlRequest(
query,
opName,
queryVariables,
extensions,
serverRequest.uri(), serverRequest.headers().asHttpHeaders(),
this.idGenerator.generateId().toString(), LocaleContextHolder.getLocale());
if (LOG.isDebugEnabled()) {
LOG.debug("Executing {}", graphQlRequest);
}
Mono responseMono = this.graphQlHandler.handleRequest(graphQlRequest)
.map(response -> {
if (LOG.isDebugEnabled()) {
LOG.debug("Execution complete");
}
ServerResponse.BodyBuilder builder = ServerResponse.ok();
builder.headers(headers -> headers.putAll(response.getResponseHeaders()));
builder.contentType(selectResponseMediaType(serverRequest));
return builder.body(response.toMap());
});
return ServerResponse.async(responseMono);
}
private T readJson(Optional string, TypeReference t) {
if (string.isEmpty()) {
return (T) new HashMap();
}
try {
return objectMapper.readValue(string.get(), t);
}
catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
private static Map readMultipartBody(ServerRequest request) {
try {
AbstractMultipartHttpServletRequest abstractMultipartHttpServletRequest = (AbstractMultipartHttpServletRequest) request.servletRequest();
return abstractMultipartHttpServletRequest.getFileMap();
}
catch (RuntimeException ex) {
throw new ServerWebInputException("Error while reading request parts", null, ex);
}
}
private static MediaType selectResponseMediaType(ServerRequest serverRequest) {
for (MediaType accepted : serverRequest.headers().accept()) {
if (SUPPORTED_RESPONSE_MEDIA_TYPES.contains(accepted)) {
return accepted;
}
}
return MediaType.APPLICATION_JSON;
}
}
// As in DGS, this is borrowed from https://github.com/graphql-java-kickstart/graphql-java-servlet/blob/eb4dfdb5c0198adc1b4d4466c3b4ea4a77def5d1/graphql-java-servlet/src/main/java/graphql/kickstart/servlet/core/internal/VariableMapper.java
class MultipartVariableMapper {
private static final Pattern PERIOD = Pattern.compile("\\.");
private static final Mapper> MAP_MAPPER = new Mapper<>() {
@Override
public Object set(Map location, String target, MultipartFile value) {
return location.put(target, value);
}
@Override
public Object recurse(Map location, String target) {
return location.get(target);
}
};
private static final Mapper> LIST_MAPPER = new Mapper<>() {
@Override
public Object set(List location, String target, MultipartFile value) {
return location.set(Integer.parseInt(target), value);
}
@Override
public Object recurse(List location, String target) {
return location.get(Integer.parseInt(target));
}
};
@SuppressWarnings({"unchecked", "rawtypes"})
public static void mapVariable(String objectPath, Map variables, MultipartFile part) {
String[] segments = PERIOD.split(objectPath);
if (segments.length < 2) {
throw new RuntimeException("object-path in map must have at least two segments");
}
else if (!"variables".equals(segments[0])) {
throw new RuntimeException("can only map into variables");
}
Object currentLocation = variables;
for (int i = 1; i < segments.length; i++) {
String segmentName = segments[i];
Mapper mapper = determineMapper(currentLocation, objectPath, segmentName);
if (i == segments.length - 1) {
if (null != mapper.set(currentLocation, segmentName, part)) {
throw new RuntimeException("expected null value when mapping " + objectPath);
}
}
else {
currentLocation = mapper.recurse(currentLocation, segmentName);
if (null == currentLocation) {
throw new RuntimeException(
"found null intermediate value when trying to map " + objectPath);
}
}
}
}
private static Mapper> determineMapper(Object currentLocation, String objectPath, String segmentName) {
if (currentLocation instanceof Map) {
return MAP_MAPPER;
}
else if (currentLocation instanceof List) {
return LIST_MAPPER;
}
throw new RuntimeException("expected a map or list at " + segmentName + " when trying to map " + objectPath);
}
interface Mapper {
Object set(T location, String target, MultipartFile value);
Object recurse(T location, String target);
}
}
// It's possible to remove this class if there was a extra constructor in WebGraphQlRequest
class MultipartGraphQlRequest extends WebGraphQlRequest implements ExecutionGraphQlRequest {
private final String document;
private final String operationName;
private final Map variables;
private final Map extensions;
public MultipartGraphQlRequest(
String query,
String operationName,
Map variables,
Map extensions,
URI uri, HttpHeaders headers,
String id, Locale locale) {
super(
uri,
headers,
null,
null,
Map.of(), // attributes
Map.of("query", query), // body
id,
locale);
this.document = query;
this.operationName = operationName;
this.variables = variables;
this.extensions = extensions;
}
@Override
public String getDocument() {
return document;
}
@Override
public String getOperationName() {
return operationName;
}
@Override
public Map getVariables() {
return variables;
}
@Override
public Map getExtensions() {
return extensions;
}
}