Skip to content

Commit 036c9f2

Browse files
authored
Merge pull request #15858 from CROSP/BAEL-7256
BAEL-7256 Upload Files With GraphQL in_Java
2 parents 35f4504 + 4a08e68 commit 036c9f2

File tree

10 files changed

+435
-0
lines changed

10 files changed

+435
-0
lines changed

spring-boot-modules/spring-boot-graphql/pom.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,16 @@
108108
<version>${jsonassert.version}</version>
109109
<scope>test</scope>
110110
</dependency>
111+
<dependency>
112+
<groupId>javax.servlet</groupId>
113+
<artifactId>javax.servlet-api</artifactId>
114+
<version>${servlet.version}</version>
115+
<scope>provided</scope>
116+
</dependency>
111117
</dependencies>
112118

113119
<properties>
120+
<servlet.version>4.0.1</servlet.version>
114121
<protobuf.version>3.19.2</protobuf.version>
115122
<protobuf-plugin.version>0.6.1</protobuf-plugin.version>
116123
<grpc.version>1.62.2</grpc.version>
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package com.baeldung.graphql.fileupload;
2+
3+
import org.springframework.stereotype.Service;
4+
import org.springframework.web.multipart.MultipartFile;
5+
6+
import java.io.IOException;
7+
import java.nio.file.Files;
8+
import java.nio.file.Path;
9+
import java.nio.file.Paths;
10+
import java.util.Objects;
11+
12+
@Service
13+
public class FileStorageService {
14+
15+
private final Path rootLocation = Paths.get("uploads");
16+
17+
public FileStorageService() {
18+
try {
19+
Files.createDirectories(rootLocation);
20+
} catch (IOException e) {
21+
throw new RuntimeException("Could not initialize storage location", e);
22+
}
23+
}
24+
25+
public String store(MultipartFile file, String description) {
26+
try {
27+
if (file.isEmpty()) {
28+
throw new RuntimeException("Failed to store empty file.");
29+
}
30+
Path destinationFile = rootLocation.resolve(
31+
Paths.get(Objects.requireNonNull(file.getOriginalFilename())))
32+
.normalize().toAbsolutePath();
33+
file.transferTo(destinationFile);
34+
return String.format("File uploaded successfully: %s with description: %s", destinationFile, description);
35+
} catch (IOException e) {
36+
throw new RuntimeException("Failed to store file.", e);
37+
}
38+
}
39+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.baeldung.graphql.fileupload;
2+
3+
import graphql.schema.DataFetcher;
4+
import graphql.schema.DataFetchingEnvironment;
5+
import org.springframework.stereotype.Component;
6+
import org.springframework.web.multipart.MultipartFile;
7+
8+
import java.io.IOException;
9+
import java.nio.file.Files;
10+
import java.nio.file.Path;
11+
import java.nio.file.Paths;
12+
import java.util.Objects;
13+
14+
@Component
15+
public class FileUploadDataFetcher implements DataFetcher<String> {
16+
private final FileStorageService fileStorageService;
17+
18+
public FileUploadDataFetcher(FileStorageService fileStorageService) {
19+
this.fileStorageService = fileStorageService;
20+
}
21+
22+
@Override
23+
public String get(DataFetchingEnvironment environment) {
24+
MultipartFile file = environment.getArgument("file");
25+
String description = environment.getArgument("description");
26+
String storedFilePath = fileStorageService.store(file, description);
27+
return String.format("File stored at: %s, Description: %s", storedFilePath, description);
28+
}
29+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.baeldung.graphql.fileupload;
2+
3+
import org.springframework.boot.SpringApplication;
4+
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
5+
import org.springframework.boot.autoconfigure.SpringBootApplication;
6+
import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
7+
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
8+
9+
@SpringBootApplication
10+
@EnableAutoConfiguration(exclude = {SecurityAutoConfiguration.class, HibernateJpaAutoConfiguration.class})
11+
public class GraphqlFileUploadApplication {
12+
public static void main(String[] args) {
13+
System.setProperty("spring.profiles.default", "file-upload");
14+
SpringApplication.run(GraphqlFileUploadApplication.class, args);
15+
}
16+
}
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
package com.baeldung.graphql.fileupload;
2+
3+
import org.apache.commons.logging.Log;
4+
import org.apache.commons.logging.LogFactory;
5+
import org.springframework.http.HttpHeaders;
6+
import org.springframework.http.HttpInputMessage;
7+
import org.springframework.http.converter.GenericHttpMessageConverter;
8+
import org.springframework.util.StringUtils;
9+
import org.springframework.web.multipart.MultipartFile;
10+
import org.springframework.web.multipart.MultipartHttpServletRequest;
11+
import reactor.core.publisher.Mono;
12+
13+
import org.springframework.context.i18n.LocaleContextHolder;
14+
import org.springframework.core.ParameterizedTypeReference;
15+
import org.springframework.graphql.server.WebGraphQlHandler;
16+
import org.springframework.graphql.server.WebGraphQlRequest;
17+
import org.springframework.http.MediaType;
18+
import org.springframework.util.AlternativeJdkIdGenerator;
19+
import org.springframework.util.Assert;
20+
import org.springframework.util.IdGenerator;
21+
import org.springframework.web.servlet.function.ServerRequest;
22+
import org.springframework.web.servlet.function.ServerResponse;
23+
24+
import javax.servlet.ServletException;
25+
import javax.servlet.http.HttpServletRequest;
26+
import javax.servlet.http.Part;
27+
import java.io.IOException;
28+
import java.io.InputStream;
29+
import java.lang.reflect.Type;
30+
import java.util.*;
31+
32+
import static org.springframework.http.MediaType.APPLICATION_GRAPHQL;
33+
34+
public class MultipartGraphQlHttpHandler {
35+
36+
private static final Log logger = LogFactory.getLog(MultipartGraphQlHttpHandler.class);
37+
38+
private static final ParameterizedTypeReference<Map<String, Object>> MAP_PARAMETERIZED_TYPE_REF = new ParameterizedTypeReference<Map<String, Object>>() {
39+
};
40+
41+
private static final ParameterizedTypeReference<Map<String, List<String>>> LIST_PARAMETERIZED_TYPE_REF = new ParameterizedTypeReference<Map<String, List<String>>>() {
42+
};
43+
44+
public static final List<MediaType> SUPPORTED_MEDIA_TYPES = Arrays.asList(APPLICATION_GRAPHQL, MediaType.APPLICATION_JSON, MediaType.APPLICATION_GRAPHQL);
45+
46+
private final IdGenerator idGenerator = new AlternativeJdkIdGenerator();
47+
48+
private final WebGraphQlHandler graphQlHandler;
49+
50+
private final GenericHttpMessageConverter genericHttpMessageConverter;
51+
52+
public MultipartGraphQlHttpHandler(WebGraphQlHandler graphQlHandler, GenericHttpMessageConverter genericHttpMessageConverter) {
53+
Assert.notNull(graphQlHandler, "WebGraphQlHandler is required");
54+
Assert.notNull(genericHttpMessageConverter, "GenericHttpMessageConverter is required");
55+
this.graphQlHandler = graphQlHandler;
56+
this.genericHttpMessageConverter = genericHttpMessageConverter;
57+
}
58+
59+
public ServerResponse handleMultipartRequest(ServerRequest serverRequest) throws ServletException {
60+
HttpServletRequest httpServletRequest = serverRequest.servletRequest();
61+
62+
Map<String, Object> inputQuery = Optional.ofNullable(this.<Map<String, Object>>deserializePart(httpServletRequest, "operations", MAP_PARAMETERIZED_TYPE_REF.getType())).orElse(new HashMap<>());
63+
64+
final Map<String, Object> queryVariables = getFromMapOrEmpty(inputQuery, "variables");
65+
final Map<String, Object> extensions = getFromMapOrEmpty(inputQuery, "extensions");
66+
67+
Map<String, MultipartFile> fileParams = readMultipartFiles(httpServletRequest);
68+
69+
Map<String, List<String>> fileMappings = Optional.ofNullable(this.<Map<String, List<String>>>deserializePart(httpServletRequest, "map", LIST_PARAMETERIZED_TYPE_REF.getType())).orElse(new HashMap<>());
70+
71+
fileMappings.forEach((String fileKey, List<String> objectPaths) -> {
72+
MultipartFile file = fileParams.get(fileKey);
73+
if (file != null) {
74+
objectPaths.forEach((String objectPath) -> {
75+
MultipartVariableMapper.mapVariable(objectPath, queryVariables, file);
76+
});
77+
}
78+
});
79+
80+
String query = (String) inputQuery.get("query");
81+
String opName = (String) inputQuery.get("operationName");
82+
83+
Map<String, Object> body = new HashMap<>();
84+
body.put("query", query);
85+
body.put("operationName", StringUtils.hasText(opName) ? opName : "");
86+
body.put("variables", queryVariables);
87+
body.put("extensions", extensions);
88+
89+
WebGraphQlRequest graphQlRequest = new WebGraphQlRequest(serverRequest.uri(), serverRequest.headers().asHttpHeaders(), body, this.idGenerator.generateId().toString(), LocaleContextHolder.getLocale());
90+
91+
if (logger.isDebugEnabled()) {
92+
logger.debug("Executing: " + graphQlRequest);
93+
}
94+
95+
Mono<ServerResponse> responseMono = this.graphQlHandler.handleRequest(graphQlRequest).map(response -> {
96+
if (logger.isDebugEnabled()) {
97+
logger.debug("Execution complete");
98+
}
99+
ServerResponse.BodyBuilder builder = ServerResponse.ok();
100+
builder.headers(headers -> headers.putAll(response.getResponseHeaders()));
101+
builder.contentType(selectResponseMediaType(serverRequest));
102+
return builder.body(response.toMap());
103+
});
104+
105+
return ServerResponse.async(responseMono);
106+
}
107+
108+
private static class JsonMultipartInputMessage implements HttpInputMessage {
109+
110+
private final Part part;
111+
112+
JsonMultipartInputMessage(Part part) {
113+
this.part = part;
114+
}
115+
116+
@Override
117+
public InputStream getBody() throws IOException {
118+
return this.part.getInputStream();
119+
}
120+
121+
@Override
122+
public HttpHeaders getHeaders() {
123+
HttpHeaders httpHeaders = new HttpHeaders();
124+
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
125+
return httpHeaders;
126+
}
127+
}
128+
129+
private <T> T deserializePart(HttpServletRequest httpServletRequest, String name, Type type) {
130+
try {
131+
Part part = httpServletRequest.getPart(name);
132+
if (part == null) {
133+
return null;
134+
}
135+
return (T) this.genericHttpMessageConverter.read(type, null, new JsonMultipartInputMessage(part));
136+
} catch (IOException | ServletException e) {
137+
throw new RuntimeException(e);
138+
}
139+
}
140+
141+
@SuppressWarnings("unchecked")
142+
private Map<String, Object> getFromMapOrEmpty(Map<String, Object> input, String key) {
143+
if (input.containsKey(key)) {
144+
return (Map<String, Object>) input.get(key);
145+
} else {
146+
return new HashMap<>();
147+
}
148+
}
149+
150+
private static Map<String, MultipartFile> readMultipartFiles(HttpServletRequest httpServletRequest) {
151+
Assert.isInstanceOf(MultipartHttpServletRequest.class, httpServletRequest, "Request should be of type MultipartHttpServletRequest");
152+
MultipartHttpServletRequest multipartHttpServletRequest = (MultipartHttpServletRequest) httpServletRequest;
153+
return multipartHttpServletRequest.getFileMap();
154+
}
155+
156+
private static MediaType selectResponseMediaType(ServerRequest serverRequest) {
157+
for (MediaType accepted : serverRequest.headers().accept()) {
158+
if (SUPPORTED_MEDIA_TYPES.contains(accepted)) {
159+
return accepted;
160+
}
161+
}
162+
return MediaType.APPLICATION_JSON;
163+
}
164+
165+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package com.baeldung.graphql.fileupload;
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper;
4+
import graphql.schema.GraphQLScalarType;
5+
import org.springframework.boot.autoconfigure.graphql.GraphQlProperties;
6+
import org.springframework.context.annotation.Bean;
7+
import org.springframework.context.annotation.Configuration;
8+
import org.springframework.core.annotation.Order;
9+
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
10+
import org.springframework.graphql.server.WebGraphQlHandler;
11+
import org.springframework.http.MediaType;
12+
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
13+
import org.springframework.web.servlet.function.RequestPredicates;
14+
import org.springframework.web.servlet.function.RouterFunction;
15+
import org.springframework.web.servlet.function.RouterFunctions;
16+
import org.springframework.web.servlet.function.ServerResponse;
17+
18+
import static com.baeldung.graphql.fileupload.MultipartGraphQlHttpHandler.SUPPORTED_MEDIA_TYPES;
19+
import static graphql.schema.idl.TypeRuntimeWiring.newTypeWiring;
20+
import static org.springframework.http.MediaType.MULTIPART_FORM_DATA;
21+
22+
@Configuration
23+
public class MultipartGraphQlWebMvcAutoconfiguration {
24+
25+
private final FileUploadDataFetcher fileUploadDataFetcher;
26+
27+
public MultipartGraphQlWebMvcAutoconfiguration(FileUploadDataFetcher fileUploadDataFetcher) {
28+
this.fileUploadDataFetcher = fileUploadDataFetcher;
29+
}
30+
31+
@Bean
32+
public RuntimeWiringConfigurer runtimeWiringConfigurer() {
33+
return (builder) -> builder
34+
.type(newTypeWiring("Mutation").dataFetcher("uploadFile", fileUploadDataFetcher))
35+
.scalar(GraphQLScalarType.newScalar()
36+
.name("Upload")
37+
.coercing(new UploadCoercing())
38+
.build());
39+
}
40+
41+
@Bean
42+
@Order(1)
43+
public RouterFunction<ServerResponse> graphQlMultipartRouterFunction(
44+
GraphQlProperties properties,
45+
WebGraphQlHandler webGraphQlHandler,
46+
ObjectMapper objectMapper
47+
) {
48+
String path = properties.getPath();
49+
RouterFunctions.Builder builder = RouterFunctions.route();
50+
MultipartGraphQlHttpHandler graphqlMultipartHandler = new MultipartGraphQlHttpHandler(webGraphQlHandler, new MappingJackson2HttpMessageConverter(objectMapper));
51+
builder = builder.POST(path, RequestPredicates.contentType(MULTIPART_FORM_DATA)
52+
.and(RequestPredicates.accept(SUPPORTED_MEDIA_TYPES.toArray(new MediaType[]{}))), graphqlMultipartHandler::handleMultipartRequest);
53+
return builder.build();
54+
}
55+
}

0 commit comments

Comments
 (0)